From 6391d759198a96491ad053db29995b6b06ee7ba6 Mon Sep 17 00:00:00 2001 From: Yuriy Sannikov Date: Wed, 26 May 2021 20:28:14 +0300 Subject: [PATCH 001/750] Refactor ModbusRegisterSensor class to get hub and configuration (#50234) * refactor ModbusRegisterSensor to match the ModbusSwitch interface * Please pylint, mypy etc. * Remove PLATFORM. Co-authored-by: jan Iversen --- homeassistant/components/modbus/__init__.py | 5 +- homeassistant/components/modbus/sensor.py | 168 +---------- homeassistant/components/modbus/validators.py | 86 ++++++ tests/components/modbus/conftest.py | 10 +- .../{test_modbus_sensor.py => test_sensor.py} | 281 +++++++++--------- 5 files changed, 257 insertions(+), 293 deletions(-) create mode 100644 homeassistant/components/modbus/validators.py rename tests/components/modbus/{test_modbus_sensor.py => test_sensor.py} (77%) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d1c3c1a0c8a..b9765f5e5ee 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -104,6 +104,7 @@ from .const import ( PLATFORMS, ) from .modbus import async_modbus_setup +from .validators import sensor_schema_validator _LOGGER = logging.getLogger(__name__) @@ -347,7 +348,9 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]), - vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.All(SENSOR_SCHEMA, sensor_schema_validator)] + ), vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), vol.Optional(CONF_FANS): vol.All(cv.ensure_list, [FAN_SCHEMA]), } diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 85bb591711c..2cc7d9223dc 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,99 +3,39 @@ from __future__ import annotations import logging import struct +from typing import Any -import voluptuous as vol - -from homeassistant.components.sensor import ( - DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, - SensorEntity, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( - CONF_ADDRESS, CONF_COUNT, - CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, - CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SLAVE, CONF_STRUCTURE, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import number from .base_platform import BasePlatform from .const import ( - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, CONF_DATA_TYPE, - CONF_HUB, - CONF_INPUT_TYPE, CONF_PRECISION, - CONF_REGISTER, - CONF_REGISTER_TYPE, - CONF_REGISTERS, - CONF_REVERSE_ORDER, CONF_SCALE, CONF_SWAP, CONF_SWAP_BYTE, - CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, - DATA_TYPE_CUSTOM, - DATA_TYPE_FLOAT, - DATA_TYPE_INT, DATA_TYPE_STRING, - DATA_TYPE_UINT, - DEFAULT_HUB, - DEFAULT_SCAN_INTERVAL, - DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) +from .modbus import ModbusHub PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_REGISTERS): [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_REGISTER): cv.positive_int, - vol.Optional(CONF_COUNT, default=1): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( - [ - DATA_TYPE_INT, - DATA_TYPE_UINT, - DATA_TYPE_FLOAT, - DATA_TYPE_STRING, - DATA_TYPE_CUSTOM, - ] - ), - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_OFFSET, default=0): number, - vol.Optional(CONF_PRECISION, default=0): cv.positive_int, - vol.Optional( - CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING - ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), - vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, - vol.Optional(CONF_SCALE, default=1): number, - vol.Optional(CONF_SLAVE): cv.positive_int, - vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } - ] - } -) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -105,92 +45,15 @@ async def async_setup_platform( """Set up the Modbus sensors.""" sensors = [] - #  check for old config: - if discovery_info is None: - _LOGGER.warning( - "Sensor configuration is deprecated, will be removed in a future release" - ) - discovery_info = { - CONF_NAME: "no name", - CONF_SENSORS: config[CONF_REGISTERS], - } - for entry in discovery_info[CONF_SENSORS]: - entry[CONF_ADDRESS] = entry[CONF_REGISTER] - entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE] - del entry[CONF_REGISTER] - del entry[CONF_REGISTER_TYPE] + if discovery_info is None: # pragma: no cover + return for entry in discovery_info[CONF_SENSORS]: - if entry[CONF_DATA_TYPE] == DATA_TYPE_STRING: - structure = str(entry[CONF_COUNT] * 2) + "s" - elif entry[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: - try: - structure = f">{DEFAULT_STRUCT_FORMAT[entry[CONF_DATA_TYPE]][entry[CONF_COUNT]]}" - except KeyError: - _LOGGER.error( - "Unable to detect data type for %s sensor, try a custom type", - entry[CONF_NAME], - ) - continue - else: - structure = entry.get(CONF_STRUCTURE) + hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + sensors.append(ModbusRegisterSensor(hub, entry)) - try: - size = struct.calcsize(structure) - except struct.error as err: - _LOGGER.error("Error in sensor %s structure: %s", entry[CONF_NAME], err) - continue - - bytecount = entry[CONF_COUNT] * 2 - if bytecount != size: - _LOGGER.error( - "Structure request %d bytes, but %d registers have a size of %d bytes", - size, - entry[CONF_COUNT], - bytecount, - ) - continue - - if CONF_REVERSE_ORDER in entry: - if entry[CONF_REVERSE_ORDER]: - entry[CONF_SWAP] = CONF_SWAP_WORD - else: - entry[CONF_SWAP] = CONF_SWAP_NONE - del entry[CONF_REVERSE_ORDER] - if entry.get(CONF_SWAP) != CONF_SWAP_NONE: - if entry[CONF_SWAP] == CONF_SWAP_BYTE: - regs_needed = 1 - else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD - regs_needed = 2 - if ( - entry[CONF_COUNT] < regs_needed - or (entry[CONF_COUNT] % regs_needed) != 0 - ): - _LOGGER.error( - "Error in sensor %s swap(%s) not possible due to count: %d", - entry[CONF_NAME], - entry[CONF_SWAP], - entry[CONF_COUNT], - ) - continue - if CONF_HUB in entry: - # from old config! - hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] - else: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] - if CONF_SCAN_INTERVAL not in entry: - entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - sensors.append( - ModbusRegisterSensor( - hub, - entry, - structure, - ) - ) - - if not sensors: - return - async_add_entities(sensors) + if len(sensors) > 0: + async_add_entities(sensors) class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): @@ -198,21 +61,18 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): def __init__( self, - hub, - entry, - structure, - ): + hub: ModbusHub, + entry: dict[str, Any], + ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._register = self._address - self._register_type = self._input_type self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._count = int(entry[CONF_COUNT]) self._swap = entry[CONF_SWAP] self._scale = entry[CONF_SCALE] self._offset = entry[CONF_OFFSET] self._precision = entry[CONF_PRECISION] - self._structure = structure + self._structure = entry.get(CONF_STRUCTURE) self._data_type = entry[CONF_DATA_TYPE] async def async_added_to_hass(self): @@ -252,7 +112,7 @@ class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval result = await self._hub.async_pymodbus_call( - self._slave, self._register, self._count, self._register_type + self._slave, self._address, self._count, self._input_type ) if result is None: self._available = False diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py new file mode 100644 index 00000000000..cd0c4524c74 --- /dev/null +++ b/homeassistant/components/modbus/validators.py @@ -0,0 +1,86 @@ +"""Validate Modbus configuration.""" +import logging +import struct + +from voluptuous import Invalid + +from homeassistant.const import CONF_COUNT, CONF_NAME, CONF_STRUCTURE + +from .const import ( + CONF_DATA_TYPE, + CONF_REVERSE_ORDER, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + DATA_TYPE_CUSTOM, + DATA_TYPE_STRING, + DEFAULT_STRUCT_FORMAT, +) + +_LOGGER = logging.getLogger(__name__) + + +def sensor_schema_validator(config): + """Sensor schema validator.""" + + if config[CONF_DATA_TYPE] == DATA_TYPE_STRING: + structure = str(config[CONF_COUNT] * 2) + "s" + elif config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: + try: + structure = ( + f">{DEFAULT_STRUCT_FORMAT[config[CONF_DATA_TYPE]][config[CONF_COUNT]]}" + ) + except KeyError: + raise Invalid( + f"Unable to detect data type for {config[CONF_NAME]} sensor, try a custom type" + ) from KeyError + else: + structure = config.get(CONF_STRUCTURE) + + if not structure: + raise Invalid( + f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty " + f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`" + ) + + try: + size = struct.calcsize(structure) + except struct.error as err: + raise Invalid( + f"Error in sensor {config[CONF_NAME]} structure: {str(err)}" + ) from err + + bytecount = config[CONF_COUNT] * 2 + if bytecount != size: + raise Invalid( + f"Structure request {size} bytes, " + f"but {config[CONF_COUNT]} registers have a size of {bytecount} bytes" + ) + + swap_type = config.get(CONF_SWAP) + + if CONF_REVERSE_ORDER in config: + if config[CONF_REVERSE_ORDER]: + swap_type = CONF_SWAP_WORD + else: + swap_type = CONF_SWAP_NONE + del config[CONF_REVERSE_ORDER] + + if config.get(CONF_SWAP) != CONF_SWAP_NONE: + if swap_type == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + if config[CONF_COUNT] < regs_needed or (config[CONF_COUNT] % regs_needed) != 0: + raise Invalid( + f"Error in sensor {config[CONF_NAME]} swap({swap_type}) " + f"not possible due to the registers " + f"count: {config[CONF_COUNT]}, needed: {regs_needed}" + ) + + return { + **config, + CONF_STRUCTURE: structure, + CONF_SWAP: swap_type, + } diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d3ae1286ef1..d066e33437c 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -80,6 +80,7 @@ async def base_test( config_modbus=None, scan_interval=None, expect_init_to_fail=False, + expect_setup_to_fail=False, ): """Run test on device for given config.""" @@ -131,7 +132,10 @@ async def base_test( {array_name_discovery: [{**config_device}]} ) config_device = None - assert await async_setup_component(hass, DOMAIN, config_modbus) + assert ( + await async_setup_component(hass, DOMAIN, config_modbus) + is not expect_setup_to_fail + ) await hass.async_block_till_done() # setup platform old style @@ -151,7 +155,7 @@ async def base_test( assert await async_setup_component(hass, entity_domain, config_device) await hass.async_block_till_done() - assert DOMAIN in hass.config.components + assert (DOMAIN in hass.config.components) is not expect_setup_to_fail if config_device is not None: entity_id = f"{entity_domain}.{device_name}" device = hass.states.get(entity_id) @@ -184,6 +188,7 @@ async def base_config_test( method_discovery=False, config_modbus=None, expect_init_to_fail=False, + expect_setup_to_fail=False, ): """Check config of device for given config.""" @@ -200,6 +205,7 @@ async def base_config_test( check_config_only=True, config_modbus=config_modbus, expect_init_to_fail=expect_init_to_fail, + expect_setup_to_fail=expect_setup_to_fail, ) diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_sensor.py similarity index 77% rename from tests/components/modbus/test_modbus_sensor.py rename to tests/components/modbus/test_sensor.py index cb784ac46b3..bb92a6972f5 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -9,8 +9,6 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_PRECISION, - CONF_REGISTER, - CONF_REGISTER_TYPE, CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, @@ -45,115 +43,58 @@ from tests.common import mock_restore_cache @pytest.mark.parametrize( - "do_discovery, do_config", + "do_config", [ - ( - False, - { - CONF_REGISTER: 51, - }, - ), - ( - False, - { - CONF_REGISTER: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, - CONF_OFFSET: 0, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DEVICE_CLASS: "battery", - }, - ), - ( - False, - { - CONF_REGISTER: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, - CONF_OFFSET: 0, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DEVICE_CLASS: "battery", - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, - CONF_OFFSET: 0, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DEVICE_CLASS: "battery", - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, - CONF_OFFSET: 0, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DEVICE_CLASS: "battery", - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_COUNT: 1, - CONF_SWAP: CONF_SWAP_NONE, - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_COUNT: 1, - CONF_SWAP: CONF_SWAP_BYTE, - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_COUNT: 2, - CONF_SWAP: CONF_SWAP_WORD, - }, - ), - ( - True, - { - CONF_ADDRESS: 51, - CONF_COUNT: 2, - CONF_SWAP: CONF_SWAP_WORD_BYTE, - }, - ), + { + CONF_ADDRESS: 51, + }, + { + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_COUNT: 1, + CONF_DATA_TYPE: "int", + CONF_PRECISION: 0, + CONF_SCALE: 1, + CONF_REVERSE_ORDER: False, + CONF_OFFSET: 0, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DEVICE_CLASS: "battery", + }, + { + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_COUNT: 1, + CONF_DATA_TYPE: "int", + CONF_PRECISION: 0, + CONF_SCALE: 1, + CONF_REVERSE_ORDER: False, + CONF_OFFSET: 0, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_DEVICE_CLASS: "battery", + }, + { + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_NONE, + }, + { + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_BYTE, + }, + { + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD, + }, + { + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + }, ], ) -async def test_config_sensor(hass, do_discovery, do_config): +async def test_config_sensor(hass, do_config): """Run test for sensor.""" sensor_name = "test_sensor" config_sensor = { @@ -167,36 +108,87 @@ async def test_config_sensor(hass, do_discovery, do_config): SENSOR_DOMAIN, CONF_SENSORS, CONF_REGISTERS, - method_discovery=do_discovery, + method_discovery=True, ) @pytest.mark.parametrize( - "do_config", + "do_config,error_message", [ - { - CONF_ADDRESS: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, - }, - { - CONF_ADDRESS: 1234, - CONF_COUNT: 8, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">no struct", - }, - { - CONF_ADDRESS: 1234, - CONF_COUNT: 2, - CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, - CONF_STRUCTURE: ">4f", - }, + ( + { + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + }, + "Unable to detect data type for test_sensor sensor, try a custom type", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_COUNT: 8, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">no struct", + }, + "Error in sensor test_sensor structure: bad char in struct format", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_COUNT: 2, + CONF_PRECISION: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">4f", + }, + "Structure request 16 bytes, but 2 registers have a size of 4 bytes", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "invalid", + }, + "Error in sensor test_sensor structure: bad char in struct format", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "", + }, + "Error in sensor test_sensor. The `structure` field can not be empty if the parameter `data_type` is set to the `custom`", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 4, + CONF_SWAP: CONF_SWAP_NONE, + CONF_STRUCTURE: "1s", + }, + "Structure request 1 bytes, but 4 registers have a size of 8 bytes", + ), + ( + { + CONF_ADDRESS: 1234, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_COUNT: 1, + CONF_STRUCTURE: "2s", + CONF_SWAP: CONF_SWAP_WORD, + }, + "Error in sensor test_sensor swap(word) not possible due to the registers count: 1, needed: 2", + ), ], ) -async def test_config_wrong_struct_sensor(hass, do_config): +async def test_config_wrong_struct_sensor( + hass, caplog, do_config, error_message, mock_pymodbus +): """Run test for sensor with wrong struct.""" sensor_name = "test_sensor" @@ -204,6 +196,9 @@ async def test_config_wrong_struct_sensor(hass, do_config): CONF_NAME: sensor_name, **do_config, } + caplog.set_level(logging.WARNING) + caplog.clear() + await base_config_test( hass, config_sensor, @@ -212,8 +207,11 @@ async def test_config_wrong_struct_sensor(hass, do_config): CONF_SENSORS, None, method_discovery=True, + expect_setup_to_fail=True, ) + assert error_message in "".join(caplog.messages) + @pytest.mark.parametrize( "cfg,regs,expected", @@ -592,10 +590,21 @@ async def test_restore_state_sensor(hass): @pytest.mark.parametrize( - "swap_type", - [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE], + "swap_type, error_message", + [ + ( + CONF_SWAP_WORD, + "Error in sensor modbus_test_sensor swap(word) not possible due to the registers count: 1, needed: 2", + ), + ( + CONF_SWAP_WORD_BYTE, + "Error in sensor modbus_test_sensor swap(word_byte) not possible due to the registers count: 1, needed: 2", + ), + ], ) -async def test_swap_sensor_wrong_config(hass, caplog, swap_type): +async def test_swap_sensor_wrong_config( + hass, caplog, swap_type, error_message, mock_pymodbus +): """Run test for sensor swap.""" sensor_name = "modbus_test_sensor" config = { @@ -616,9 +625,9 @@ async def test_swap_sensor_wrong_config(hass, caplog, swap_type): CONF_SENSORS, None, method_discovery=True, - expect_init_to_fail=True, + expect_setup_to_fail=True, ) - assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap") + assert error_message in "".join(caplog.messages) async def test_service_sensor_update(hass, mock_pymodbus): From d829df332d8353661268f45300ac919dd8eb5116 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 May 2021 20:18:29 +0200 Subject: [PATCH 002/750] Bump version to 2021.7.0dev0 (#51116) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 00e06f1e8d0..529b0969e4a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Final MAJOR_VERSION: Final = 2021 -MINOR_VERSION: Final = 6 +MINOR_VERSION: Final = 7 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From f45bc3abc7f5c50aebc05a7c00463b2de2837358 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 27 May 2021 00:17:03 +0000 Subject: [PATCH 003/750] [ci skip] Translation update --- .../components/aemet/translations/pl.json | 9 ++++ .../components/bosch_shc/translations/es.json | 10 ++++ .../components/bosch_shc/translations/pl.json | 38 ++++++++++++++ .../components/fritz/translations/ca.json | 9 ++++ .../components/fritz/translations/es.json | 13 +++++ .../components/fritz/translations/pl.json | 20 ++++++++ .../garages_amsterdam/translations/es.json | 13 +++++ .../garages_amsterdam/translations/pl.json | 18 +++++++ .../components/goalzero/translations/pl.json | 10 +++- .../homekit_controller/translations/ca.json | 2 + .../homekit_controller/translations/pl.json | 2 + .../translations/pl.json | 1 + .../components/isy994/translations/pl.json | 8 +++ .../keenetic_ndms2/translations/ca.json | 5 +- .../keenetic_ndms2/translations/es.json | 5 +- .../keenetic_ndms2/translations/no.json | 5 +- .../keenetic_ndms2/translations/pl.json | 5 +- .../keenetic_ndms2/translations/zh-Hant.json | 5 +- .../components/kraken/translations/pl.json | 34 +++++++++++++ .../meteoclimatic/translations/ca.json | 20 ++++++++ .../meteoclimatic/translations/es.json | 13 +++++ .../meteoclimatic/translations/no.json | 20 ++++++++ .../meteoclimatic/translations/pl.json | 20 ++++++++ .../meteoclimatic/translations/zh-Hant.json | 20 ++++++++ .../components/motioneye/translations/es.json | 4 ++ .../components/motioneye/translations/et.json | 4 ++ .../components/motioneye/translations/pl.json | 4 ++ .../components/motioneye/translations/ru.json | 4 ++ .../motioneye/translations/zh-Hant.json | 4 ++ .../components/nexia/translations/pl.json | 1 + .../components/samsungtv/translations/ca.json | 4 ++ .../components/samsungtv/translations/pl.json | 17 +++++-- .../components/sia/translations/ca.json | 50 +++++++++++++++++++ .../components/sia/translations/es.json | 32 ++++++++++++ .../components/sia/translations/pl.json | 50 +++++++++++++++++++ .../totalconnect/translations/en.json | 1 + .../totalconnect/translations/es.json | 3 +- .../totalconnect/translations/et.json | 5 +- .../totalconnect/translations/pl.json | 5 +- .../totalconnect/translations/ru.json | 5 +- .../components/upnp/translations/pl.json | 10 ++++ .../components/wallbox/translations/ca.json | 22 ++++++++ .../components/wallbox/translations/es.json | 12 +++++ .../components/wallbox/translations/pl.json | 22 ++++++++ 44 files changed, 546 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/bosch_shc/translations/es.json create mode 100644 homeassistant/components/bosch_shc/translations/pl.json create mode 100644 homeassistant/components/garages_amsterdam/translations/es.json create mode 100644 homeassistant/components/garages_amsterdam/translations/pl.json create mode 100644 homeassistant/components/kraken/translations/pl.json create mode 100644 homeassistant/components/meteoclimatic/translations/ca.json create mode 100644 homeassistant/components/meteoclimatic/translations/es.json create mode 100644 homeassistant/components/meteoclimatic/translations/no.json create mode 100644 homeassistant/components/meteoclimatic/translations/pl.json create mode 100644 homeassistant/components/meteoclimatic/translations/zh-Hant.json create mode 100644 homeassistant/components/sia/translations/ca.json create mode 100644 homeassistant/components/sia/translations/es.json create mode 100644 homeassistant/components/sia/translations/pl.json create mode 100644 homeassistant/components/wallbox/translations/ca.json create mode 100644 homeassistant/components/wallbox/translations/es.json create mode 100644 homeassistant/components/wallbox/translations/pl.json diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json index 2c5c24fae2a..8531ca47fd6 100644 --- a/homeassistant/components/aemet/translations/pl.json +++ b/homeassistant/components/aemet/translations/pl.json @@ -18,5 +18,14 @@ "title": "AEMET OpenData" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Zbieraj dane ze stacji pogodowych AEMET" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json new file mode 100644 index 00000000000..71aaac2dfb2 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Par\u00e1metros de autenticaci\u00f3n SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/pl.json b/homeassistant/components/bosch_shc/translations/pl.json new file mode 100644 index 00000000000..c140bf6b6f8 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "pairing_failed": "Parowanie nie powiod\u0142o si\u0119. Sprawd\u017a, czy kontroler Bosch Smart Home jest w trybie parowania (miga dioda LED) i czy Twoje has\u0142o jest prawid\u0142owe.", + "session_error": "B\u0142\u0105d sesji: API zwr\u00f3ci\u0142o niepoprawny wynik.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Naci\u015bnij przycisk z przodu kontrolera Bosch Smart Home, a\u017c dioda LED zacznie miga\u0107. Chcesz kontynuowa\u0107 konfiguracj\u0119 {model} @ {host} z Home Assistantem?" + }, + "credentials": { + "data": { + "password": "Has\u0142o kontrolera" + } + }, + "reauth_confirm": { + "description": "Integracja Bosch_SHC wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Skonfiguruj kontroler Bosch Smart Home, aby umo\u017cliwi\u0107 monitorowanie i sterowanie za pomoc\u0105 Home Assistanta.", + "title": "Parametry uwierzytelniania SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 10926ed8348..2e240bb9833 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -51,5 +51,14 @@ "title": "Configuraci\u00f3 de FRITZ!Box Tools" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index db9b2fa5c2a..ed39b227ec8 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -38,6 +38,19 @@ }, "description": "Configurar FRITZ!Box Tools para controlar tu FRITZ!Box.\nM\u00ednimo necesario: usuario, contrase\u00f1a.", "title": "Configurar FRITZ!Box Tools - obligatorio" + }, + "user": { + "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", + "title": "Configurar las herramientas de FRITZ! Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segundos para considerar un dispositivo en 'casa'" + } } } } diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index b9f17e23624..5632ae67694 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, @@ -38,6 +39,25 @@ }, "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ! Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.", "title": "Konfiguracja narz\u0119dzi FRITZ!Box - obowi\u0105zkowe" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ!Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.", + "title": "Konfiguracja narz\u0119dzi FRITZ!Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\"" + } } } } diff --git a/homeassistant/components/garages_amsterdam/translations/es.json b/homeassistant/components/garages_amsterdam/translations/es.json new file mode 100644 index 00000000000..3bf5c176b56 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "garage_name": "Nombre del garaje" + }, + "title": "Elige un garaje para vigilar" + } + } + }, + "title": "Garajes Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/pl.json b/homeassistant/components/garages_amsterdam/translations/pl.json new file mode 100644 index 00000000000..a9f220d9bfc --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "garage_name": "Nazwa parkingu" + }, + "title": "Wybierz parking do monitorowania" + } + } + }, + "title": "Parkingi Amsterdamie" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json index 4b06301953e..3aba221bc4a 100644 --- a/homeassistant/components/goalzero/translations/pl.json +++ b/homeassistant/components/goalzero/translations/pl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,12 +11,16 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "confirm_discovery": { + "description": "Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. W ustawieniach routera nale\u017cy skonfigurowa\u0107 rezerwacj\u0119 adres\u00f3w DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.", + "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index e46e083f1bd..ff9f180c943 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Codi HomeKit incorrecte. Verifica'l i torna-ho a provar.", + "insecure_setup_code": "El codi de configuraci\u00f3 sol\u00b7licitat no \u00e9s segur per naturalesa. Aquest accessori no compleix els requisits b\u00e0sics de seguretat.", "max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.", "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb aquest dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no sigui compatible.", "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Permet la vinculaci\u00f3 amb codis de configuraci\u00f3 insegurs.", "pairing_code": "Codi de vinculaci\u00f3" }, "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.", diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index 87a7d4ef2b5..d7b5cc69cf3 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.", + "insecure_setup_code": "\u017b\u0105dany kod instalacyjny jest niezabezpieczony ze wzgl\u0119du na jego trywialny charakter. To akcesorium nie spe\u0142nia podstawowych wymaga\u0144 bezpiecze\u0144stwa.", "max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania", "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Zezwalaj na parowanie z niezabezpieczonymi kodami konfiguracji.", "pairing_code": "Kod parowania" }, "description": "Kontroler HomeKit komunikuje si\u0119 z {name} poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json index 1bf55f64477..fa8b1d856a2 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pl.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json @@ -7,6 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index c0ee1b5d3a3..4deeefe391d 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -36,5 +36,13 @@ "title": "Opcje ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY pod\u0142\u0105czony", + "host_reachable": "Host osi\u0105galny", + "last_heartbeat": "Czas ostatniego pulsu", + "websocket_status": "Status wydarzenia websocket" + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json index 364b5dad10e..0acb0ef0266 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ca.json +++ b/homeassistant/components/keenetic_ndms2/translations/ca.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "no_udn": "La informaci\u00f3 de descobriment SSDP no t\u00e9 UDN", + "not_keenetic_ndms2": "El dispositiu descobert no \u00e9s un router Keenetic" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 6b8eed6e26b..190caf9947b 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "no_udn": "La informaci\u00f3n de descubrimiento SSDP no tiene UDN", + "not_keenetic_ndms2": "El art\u00edculo descubierto no es un router Keenetic" }, "error": { "cannot_connect": "Fallo de conexi\u00f3n" }, + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/no.json b/homeassistant/components/keenetic_ndms2/translations/no.json index d37a306d9a6..52200ccc615 100644 --- a/homeassistant/components/keenetic_ndms2/translations/no.json +++ b/homeassistant/components/keenetic_ndms2/translations/no.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "no_udn": "SSDP-oppdagelsesinformasjon har ingen UDN", + "not_keenetic_ndms2": "Oppdaget element er ikke en Keenetic-router" }, "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/pl.json b/homeassistant/components/keenetic_ndms2/translations/pl.json index ba8b709301d..a96f50ecc19 100644 --- a/homeassistant/components/keenetic_ndms2/translations/pl.json +++ b/homeassistant/components/keenetic_ndms2/translations/pl.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "no_udn": "Informacje o wykrywaniu SSDP nie maj\u0105 UDN", + "not_keenetic_ndms2": "Wykryty element nie jest routerem Keenetic" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json index 2f64eee7439..616ccff6c20 100644 --- a/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json +++ b/homeassistant/components/keenetic_ndms2/translations/zh-Hant.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_udn": "SSDP \u6240\u767c\u73fe\u7684\u8cc7\u8a0a\u4e0d\u542b UDN", + "not_keenetic_ndms2": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Keenetic \u8def\u7531\u5668" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kraken/translations/pl.json b/homeassistant/components/kraken/translations/pl.json new file mode 100644 index 00000000000..288b3b1d2b2 --- /dev/null +++ b/homeassistant/components/kraken/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "step": { + "user": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji", + "tracked_asset_pairs": "\u015aledzone pary walut" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ca.json b/homeassistant/components/meteoclimatic/translations/ca.json new file mode 100644 index 00000000000..5f672d87535 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "unknown": "Error inesperat" + }, + "error": { + "not_found": "No s'han trobat dispositius a la xarxa" + }, + "step": { + "user": { + "data": { + "code": "Codi d'estaci\u00f3" + }, + "description": "Introdueix el codi d'estaci\u00f3 meteorol\u00f2gica (per exemple, ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json new file mode 100644 index 00000000000..251fcbe8e09 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "code": "C\u00f3digo de la estaci\u00f3n" + }, + "description": "Introduzca el c\u00f3digo de la estaci\u00f3n Meteoclimatic (por ejemplo, ESCAT430000000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/no.json b/homeassistant/components/meteoclimatic/translations/no.json new file mode 100644 index 00000000000..0e6e080d146 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "unknown": "Uventet feil" + }, + "error": { + "not_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "step": { + "user": { + "data": { + "code": "Stasjonskode" + }, + "description": "Angi Meteoclimatic stasjonskode (f.eks. ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/pl.json b/homeassistant/components/meteoclimatic/translations/pl.json new file mode 100644 index 00000000000..c4539bd6c8b --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "not_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "step": { + "user": { + "data": { + "code": "Kod stacji" + }, + "description": "Wpisz kod stacji Meteoclimatic (np. ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/zh-Hant.json b/homeassistant/components/meteoclimatic/translations/zh-Hant.json new file mode 100644 index 00000000000..d5c3793be7d --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "not_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "step": { + "user": { + "data": { + "code": "\u6c23\u8c61\u7ad9\u4ee3\u78bc" + }, + "description": "\u8f38\u5165 Meteoclimatic \u6c23\u8c61\u7ad9\u4ee3\u78bc\uff08\u4f8b\u5982 ESCAT4300000043206B\uff09", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index 4f749d5c6d8..d018c52515e 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -11,6 +11,10 @@ "unknown": "Error inesperado" }, "step": { + "hassio_confirm": { + "description": "\u00bfQuieres configurar Home Assistant para que se conecte al servicio motionEye proporcionado por el complemento: {addon}?", + "title": "motionEye a trav\u00e9s del complemento Home Assistant" + }, "user": { "data": { "admin_password": "Contrase\u00f1a administrador", diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json index c3e44c52974..1b0861dbc75 100644 --- a/homeassistant/components/motioneye/translations/et.json +++ b/homeassistant/components/motioneye/translations/et.json @@ -11,6 +11,10 @@ "unknown": "Tundmatu viga" }, "step": { + "hassio_confirm": { + "description": "Kas konfigureerida Home Assistanti \u00fchenduse loomiseks lisandmooduli pakutava motionEye teenusega: {addon} ?", + "title": "motionEye Home Assistanti lisandmooduli kaudu" + }, "user": { "data": { "admin_password": "Haldaja salas\u00f5na", diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json index dca40bdcd3d..296c2f963af 100644 --- a/homeassistant/components/motioneye/translations/pl.json +++ b/homeassistant/components/motioneye/translations/pl.json @@ -11,6 +11,10 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "hassio_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z motionEye przez dodatek {addon}?", + "title": "motionEye poprzez dodatek Home Assistant" + }, "user": { "data": { "admin_password": "Has\u0142o admina", diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json index a983ddcae0f..8999b0e8f82 100644 --- a/homeassistant/components/motioneye/translations/ru.json +++ b/homeassistant/components/motioneye/translations/ru.json @@ -11,6 +11,10 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "hassio_confirm": { + "description": "\u041d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 motionEye (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", + "title": "motionEye (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" + }, "user": { "data": { "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json index aa05784e53d..8c143655f2f 100644 --- a/homeassistant/components/motioneye/translations/zh-Hant.json +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -11,6 +11,10 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "hassio_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 motionEye\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 motionEye" + }, "user": { "data": { "admin_password": "Admin \u5bc6\u78bc", diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json index fc1bdf7f273..6e40e21a7f0 100644 --- a/homeassistant/components/nexia/translations/pl.json +++ b/homeassistant/components/nexia/translations/pl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marka", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index 9ccff13ae3d..aab8e05b5ac 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -5,6 +5,7 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.", "cannot_connect": "Ha fallat la connexi\u00f3", + "id_missing": "El dispositiu Samsung no t\u00e9 cap n\u00famero de s\u00e8rie.", "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" @@ -18,6 +19,9 @@ "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.", "title": "Televisor Samsung" }, + "reauth_confirm": { + "description": "Despr\u00e9s d'enviar, tens 30 segons per acceptar la finestra emergent de {device} que sol\u00b7licita autoritzaci\u00f3." + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 66d6ce3c4f3..ed117c60d21 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia telewizora, aby autoryzowa\u0107 Home Assistant.", + "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany" + "id_missing": "To urz\u0105dzenie Samsung nie ma numeru seryjnego.", + "not_supported": "To urz\u0105dzenie Samsung nie jest obecnie obs\u0142ugiwane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "{model}", + "error": { + "auth_missing": "[%key::component::samsungtv::config::abort::auth_missing%]" + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", + "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Po wys\u0142aniu \u017c\u0105dania, zaakceptuj wyskakuj\u0105ce okienko na {device} z pro\u015bb\u0105 o autoryzacj\u0119 w ci\u0105gu 30 sekund." + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json new file mode 100644 index 00000000000..a34ce4bdd0d --- /dev/null +++ b/homeassistant/components/sia/translations/ca.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "El compte no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", + "invalid_account_length": "El compte no t\u00e9 la longitud correcta, ha de tenir entre 3 i 16 car\u00e0cters.", + "invalid_key_format": "La clau no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", + "invalid_key_length": "La clau no t\u00e9 la longitud correcta, ha de tenir 16, 24 o 32 car\u00e0cters hexadecimals.", + "invalid_ping": "L'interval de refresc ha d'estar compr\u00e8s entre 1 i 1440 minuts.", + "invalid_zones": "Cal que hi hagi com a m\u00ednim 1 zona.", + "unknown": "Error inesperat" + }, + "step": { + "additional_account": { + "data": { + "account": "ID del compte", + "additional_account": "Comptes addicionals", + "encryption_key": "Clau de xifrat", + "ping_interval": "Interval de refresc (min)", + "zones": "Nombre de zones del compte" + }, + "title": "Afegeix un altre compte al port actual." + }, + "user": { + "data": { + "account": "ID del compte", + "additional_account": "Comptes addicionals", + "encryption_key": "Clau de xifrat", + "ping_interval": "Interval de refresc (min)", + "port": "Port", + "protocol": "Protocol", + "zones": "Nombre de zones del compte" + }, + "title": "Crea una connexi\u00f3 per a sistemes d'alarma basats en SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignora la comprovaci\u00f3 de marca de temps dels esdeveniments SIA", + "zones": "Nombre de zones del compte" + }, + "description": "Configura les opcions del compte: {account}", + "title": "Opcions de configuraci\u00f3 de SIA." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json new file mode 100644 index 00000000000..02d266869bf --- /dev/null +++ b/homeassistant/components/sia/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "additional_account": { + "title": "Agrega otra cuenta al puerto actual." + }, + "user": { + "data": { + "account": "ID de la cuenta", + "additional_account": "Cuentas adicionales", + "encryption_key": "Clave de encriptaci\u00f3n", + "ping_interval": "Intervalo de ping (min)", + "protocol": "Protocolo", + "zones": "N\u00famero de zonas de la cuenta" + }, + "title": "Cree una conexi\u00f3n para sistemas de alarma basados en SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA" + }, + "description": "Configure las opciones para la cuenta: {account}", + "title": "Opciones para la configuraci\u00f3n de SIA." + } + } + }, + "title": "Sistemas de alarma SIA" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/pl.json b/homeassistant/components/sia/translations/pl.json new file mode 100644 index 00000000000..d41281519d4 --- /dev/null +++ b/homeassistant/components/sia/translations/pl.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Konto nie jest warto\u015bci\u0105 szesnastkow\u0105, u\u017cyj tylko 0-9 i A-F.", + "invalid_account_length": "Konto ma niew\u0142a\u015bciw\u0105 d\u0142ugo\u015b\u0107, musi mie\u0107 od 3 do 16 znak\u00f3w.", + "invalid_key_format": "Klucz nie jest warto\u015bci\u0105 szesnastkow\u0105, u\u017cyj tylko 0-9 i A-F.", + "invalid_key_length": "Klucz ma niew\u0142a\u015bciw\u0105 d\u0142ugo\u015b\u0107, musi mie\u0107 16, 24 lub 32 znaki szesnastkowe.", + "invalid_ping": "Cz\u0119stotliwo\u015b\u0107 pingowania musi wynosi\u0107 od 1 do 1440 minut.", + "invalid_zones": "Musi istnie\u0107 co najmniej 1 strefa.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "additional_account": { + "data": { + "account": "Identyfikator konta", + "additional_account": "Dodatkowe konta", + "encryption_key": "Klucz szyfrowania", + "ping_interval": "Cz\u0119stotliwo\u015b\u0107 pingowania (min)", + "zones": "Liczba stref dla konta" + }, + "title": "Dodaj kolejne konto do bie\u017c\u0105cego portu." + }, + "user": { + "data": { + "account": "Identyfikator konta", + "additional_account": "Dodatkowe konta", + "encryption_key": "Klucz szyfrowania", + "ping_interval": "Cz\u0119stotliwo\u015b\u0107 pingowania (min)", + "port": "Port", + "protocol": "Protok\u00f3\u0142", + "zones": "Liczba stref dla konta" + }, + "title": "Tworzenie po\u0142\u0105czenia dla system\u00f3w alarmowych opartych na SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignoruj sprawdzanie znacznika czasu dla wydarze\u0144 SIA", + "zones": "Liczba stref dla konta" + }, + "description": "Ustaw opcje dla konta: {account}", + "title": "Opcje dla konfiguracji SIA." + } + } + }, + "title": "Systemy alarmowe SIA" +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index 02ea1bfccbd..05f394fbb31 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -11,6 +11,7 @@ "step": { "locations": { "data": { + "location": "Location", "usercode": "Usercode" }, "description": "Enter the usercode for this user at location {location_id}", diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 07837760a44..c4923884c43 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -11,7 +11,8 @@ "step": { "locations": { "data": { - "location": "Localizaci\u00f3n" + "location": "Localizaci\u00f3n", + "usercode": "Codigo de usuario" }, "description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n", "title": "C\u00f3digos de usuario de ubicaci\u00f3n" diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json index 3f1a15fe139..a4110f9bf0f 100644 --- a/homeassistant/components/totalconnect/translations/et.json +++ b/homeassistant/components/totalconnect/translations/et.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Asukoht" + "location": "Asukoht", + "usercode": "Kasutajakood" }, - "description": "Sisesta selle kasutaja kood selles asukohas", + "description": "Sisesta kasutaja kood asukohale {location_id}", "title": "Asukoha kasutajakoodid" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index ff2ca2351e6..03452569c28 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Lokalizacja" + "location": "Lokalizacja", + "usercode": "Kod u\u017cytkownika" }, - "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w tej lokalizacji", + "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w lokalizacji {location_id}", "title": "Kody lokalizacji u\u017cytkownika" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index a4e48ca01d4..268f620c238 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "usercode": "\u041a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u044d\u0442\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 {location_id}.", "title": "\u041a\u043e\u0434\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, "reauth_confirm": { diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index 3f66ec7c6f9..30213436d27 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -25,9 +25,19 @@ "user": { "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)", + "unique_id": "Urz\u0105dzenie", "usn": "Urz\u0105dzenie" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ca.json b/homeassistant/components/wallbox/translations/ca.json new file mode 100644 index 00000000000..55240065548 --- /dev/null +++ b/homeassistant/components/wallbox/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "station": "N\u00famero de s\u00e8rie de l'estaci\u00f3", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json new file mode 100644 index 00000000000..71e7a748955 --- /dev/null +++ b/homeassistant/components/wallbox/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "station": "N\u00famero de serie de la estaci\u00f3n" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pl.json b/homeassistant/components/wallbox/translations/pl.json new file mode 100644 index 00000000000..2728f1cae31 --- /dev/null +++ b/homeassistant/components/wallbox/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "station": "Numer seryjny stacji", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file From 38e0cbe964d7a9c114fab8bc0ade83071a12f455 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 27 May 2021 11:22:31 +0800 Subject: [PATCH 004/750] Change stream sequence number to start from 0 (#51101) * Use constants for provider strings * Add last_sequence property --- homeassistant/components/stream/__init__.py | 14 ++++--- homeassistant/components/stream/const.py | 7 +++- homeassistant/components/stream/core.py | 10 ++++- homeassistant/components/stream/hls.py | 23 ++++++----- homeassistant/components/stream/recorder.py | 10 +++-- homeassistant/components/stream/worker.py | 4 +- tests/components/stream/test_hls.py | 28 ++++++++------ tests/components/stream/test_recorder.py | 7 ++-- tests/components/stream/test_worker.py | 42 ++++++++++----------- 9 files changed, 87 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 67bfe404d7d..f71e725fcb4 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -31,8 +31,10 @@ from .const import ( ATTR_ENDPOINTS, ATTR_STREAMS, DOMAIN, + HLS_PROVIDER, MAX_SEGMENTS, OUTPUT_IDLE_TIMEOUT, + RECORDER_PROVIDER, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, ) @@ -90,7 +92,7 @@ async def async_setup(hass, config): # Setup HLS hls_endpoint = async_setup_hls(hass) - hass.data[DOMAIN][ATTR_ENDPOINTS]["hls"] = hls_endpoint + hass.data[DOMAIN][ATTR_ENDPOINTS][HLS_PROVIDER] = hls_endpoint # Setup Recorder async_setup_recorder(hass) @@ -146,7 +148,9 @@ class Stream: @callback def idle_callback(): - if (not self.keepalive or fmt == "recorder") and fmt in self._outputs: + if ( + not self.keepalive or fmt == RECORDER_PROVIDER + ) and fmt in self._outputs: self.remove_provider(self._outputs[fmt]) self.check_idle() @@ -259,19 +263,19 @@ class Stream: raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Add recorder - recorder = self.outputs().get("recorder") + recorder = self.outputs().get(RECORDER_PROVIDER) if recorder: raise HomeAssistantError( f"Stream already recording to {recorder.video_path}!" ) - recorder = self.add_provider("recorder", timeout=duration) + recorder = self.add_provider(RECORDER_PROVIDER, timeout=duration) recorder.video_path = video_path self.start() _LOGGER.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback - hls = self.outputs().get("hls") + hls = self.outputs().get(HLS_PROVIDER) if lookback > 0 and hls: num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index a2557286cf1..e1b1b610e03 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -4,13 +4,16 @@ DOMAIN = "stream" ATTR_ENDPOINTS = "endpoints" ATTR_STREAMS = "streams" -OUTPUT_FORMATS = ["hls"] +HLS_PROVIDER = "hls" +RECORDER_PROVIDER = "recorder" + +OUTPUT_FORMATS = [HLS_PROVIDER] SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output AUDIO_CODECS = {"aac", "mp3"} -FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} +FORMAT_CONTENT_TYPE = {HLS_PROVIDER: "application/vnd.apple.mpegurl"} OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 695f1d05ac3..70cd5b2eba8 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -97,6 +97,13 @@ class StreamOutput: """Return True if the output is idle.""" return self._idle_timer.idle + @property + def last_sequence(self) -> int: + """Return the last sequence number without iterating.""" + if self._segments: + return self._segments[-1].sequence + return -1 + @property def segments(self) -> list[int]: """Return current sequence from segments.""" @@ -127,8 +134,7 @@ class StreamOutput: async def recv(self) -> Segment | None: """Wait for and retrieve the latest segment.""" - last_segment = max(self.segments, default=0) - if self._cursor is None or self._cursor <= last_segment: + if self._cursor is None or self._cursor <= self.last_sequence: await self._event.wait() if not self._segments: diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 941f4407423..4968c935d72 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -3,7 +3,12 @@ from aiohttp import web from homeassistant.core import callback -from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS +from .const import ( + FORMAT_CONTENT_TYPE, + HLS_PROVIDER, + MAX_SEGMENTS, + NUM_PLAYLIST_SEGMENTS, +) from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView from .fmp4utils import get_codec_string @@ -45,12 +50,12 @@ class HlsMasterPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.add_provider("hls") + track = stream.add_provider(HLS_PROVIDER) stream.start() # Wait for a segment to be ready if not track.segments and not await track.recv(): return web.HTTPNotFound() - headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} + headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -104,12 +109,12 @@ class HlsPlaylistView(StreamView): async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" - track = stream.add_provider("hls") + track = stream.add_provider(HLS_PROVIDER) stream.start() # Wait for a segment to be ready if not track.segments and not await track.recv(): return web.HTTPNotFound() - headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} + headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -122,7 +127,7 @@ class HlsInitView(StreamView): async def handle(self, request, stream, sequence): """Return init.mp4.""" - track = stream.add_provider("hls") + track = stream.add_provider(HLS_PROVIDER) segments = track.get_segments() if not segments: return web.HTTPNotFound() @@ -139,7 +144,7 @@ class HlsSegmentView(StreamView): async def handle(self, request, stream, sequence): """Return fmp4 segment.""" - track = stream.add_provider("hls") + track = stream.add_provider(HLS_PROVIDER) segment = track.get_segment(int(sequence)) if not segment: return web.HTTPNotFound() @@ -150,7 +155,7 @@ class HlsSegmentView(StreamView): ) -@PROVIDERS.register("hls") +@PROVIDERS.register(HLS_PROVIDER) class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" @@ -161,4 +166,4 @@ class HlsStreamOutput(StreamOutput): @property def name(self) -> str: """Return provider name.""" - return "hls" + return HLS_PROVIDER diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 7d849375ece..ac5f102e625 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -12,7 +12,11 @@ from av.container import OutputContainer from homeassistant.core import HomeAssistant, callback -from .const import RECORDER_CONTAINER_FORMAT, SEGMENT_CONTAINER_FORMAT +from .const import ( + RECORDER_CONTAINER_FORMAT, + RECORDER_PROVIDER, + SEGMENT_CONTAINER_FORMAT, +) from .core import PROVIDERS, IdleTimer, Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -110,7 +114,7 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]): output.close() -@PROVIDERS.register("recorder") +@PROVIDERS.register(RECORDER_PROVIDER) class RecorderOutput(StreamOutput): """Represents HLS Output formats.""" @@ -122,7 +126,7 @@ class RecorderOutput(StreamOutput): @property def name(self) -> str: """Return provider name.""" - return "recorder" + return RECORDER_PROVIDER def prepend(self, segments: list[Segment]) -> None: """Prepend segments to existing list.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index cb6d6a6a017..6fe339c5dea 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -32,7 +32,9 @@ class SegmentBuffer: self._stream_id = 0 self._outputs_callback = outputs_callback self._outputs: list[StreamOutput] = [] - self._sequence = 0 + # sequence gets incremented before the first segment so the first segment + # has a sequence number of 0. + self._sequence = -1 self._segment_start_pts = None self._memory_file: BytesIO = cast(BytesIO, None) self._av_output: av.container.OutputContainer = None diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index f9b96a662d9..3e4b81bcc25 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -7,7 +7,11 @@ import av import pytest from homeassistant.components.stream import create_stream -from homeassistant.components.stream.const import MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS +from homeassistant.components.stream.const import ( + HLS_PROVIDER, + MAX_SEGMENTS, + NUM_PLAYLIST_SEGMENTS, +) from homeassistant.components.stream.core import Segment from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component @@ -47,7 +51,7 @@ def hls_stream(hass, hass_client): async def create_client_for_stream(stream): http_client = await hass_client() - parsed_url = urlparse(stream.endpoint_url("hls")) + parsed_url = urlparse(stream.endpoint_url(HLS_PROVIDER)) return HlsClient(http_client, parsed_url) return create_client_for_stream @@ -93,7 +97,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.add_provider("hls") + stream.add_provider(HLS_PROVIDER) stream.start() hls_client = await hls_stream(stream) @@ -134,9 +138,9 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.add_provider("hls") + stream.add_provider(HLS_PROVIDER) stream.start() - url = stream.endpoint_url("hls") + url = stream.endpoint_url(HLS_PROVIDER) http_client = await hass_client() @@ -176,7 +180,7 @@ async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) # Request stream - stream.add_provider("hls") + stream.add_provider(HLS_PROVIDER) stream.start() stream_worker_sync.resume() @@ -196,7 +200,7 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" stream = create_stream(hass, source) - track = stream.add_provider("hls") + track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 cur_time = 0 @@ -231,7 +235,7 @@ async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream): await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, STREAM_SOURCE) - stream.add_provider("hls") + stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) @@ -246,7 +250,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.add_provider(HLS_PROVIDER) hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() @@ -275,7 +279,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) @@ -316,7 +320,7 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.add_provider(HLS_PROVIDER) hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) @@ -346,7 +350,7 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy stream = create_stream(hass, STREAM_SOURCE) stream_worker_sync.pause() - hls = stream.add_provider("hls") + hls = stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 9097d03a7a9..216e02a95b9 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -15,6 +15,7 @@ import av import pytest from homeassistant.components.stream import create_stream +from homeassistant.components.stream.const import HLS_PROVIDER, RECORDER_PROVIDER from homeassistant.components.stream.core import Segment from homeassistant.components.stream.fmp4utils import get_init_and_moof_data from homeassistant.components.stream.recorder import recorder_save_worker @@ -119,7 +120,7 @@ async def test_record_lookback( stream = create_stream(hass, source) # Start an HLS feed to enable lookback - stream.add_provider("hls") + stream.add_provider(HLS_PROVIDER) stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -148,7 +149,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - recorder = stream.add_provider("recorder") + recorder = stream.add_provider(RECORDER_PROVIDER) await recorder.recv() @@ -252,7 +253,7 @@ async def test_record_stream_audio( stream = create_stream(hass, source) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") - recorder = stream.add_provider("recorder") + recorder = stream.add_provider(RECORDER_PROVIDER) while True: segment = await recorder.recv() diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d5527105a70..501ea302172 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -23,6 +23,7 @@ import av from homeassistant.components.stream import Stream from homeassistant.components.stream.const import ( + HLS_PROVIDER, MAX_MISSING_DTS, MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, @@ -31,7 +32,6 @@ from homeassistant.components.stream.worker import SegmentBuffer, stream_worker STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests -STREAM_OUTPUT_FORMAT = "hls" AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 @@ -198,7 +198,7 @@ class MockPyAv: async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.add_provider(HLS_PROVIDER) if not py_av: py_av = MockPyAv() @@ -218,7 +218,7 @@ async def async_decode_stream(hass, packets, py_av=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") segment_buffer = SegmentBuffer(stream.outputs) @@ -237,9 +237,9 @@ async def test_stream_worker_success(hass): # segment arrives, hence the subtraction of one from the sequence length. assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH assert len(decoded_stream.audio_packets) == 0 @@ -253,7 +253,7 @@ async def test_skip_out_of_order_packet(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # If skipped packet would have been the first packet of a segment, the previous # segment will be longer by a packet duration # We also may possibly lose a segment due to the shifting pts boundary @@ -273,7 +273,7 @@ async def test_skip_out_of_order_packet(hass): # Check number of segments assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET) # Check remaining segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == len(packets) - 1 assert len(decoded_stream.audio_packets) == 0 @@ -290,9 +290,9 @@ async def test_discard_old_packets(hass): # Check number of segments assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX assert len(decoded_stream.audio_packets) == 0 @@ -309,9 +309,9 @@ async def test_packet_overflow(hass): # Check number of segments assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX assert len(decoded_stream.audio_packets) == 0 @@ -332,9 +332,9 @@ async def test_skip_initial_bad_packets(hass): (num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET ) # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == num_packets - num_bad_packets assert len(decoded_stream.audio_packets) == 0 @@ -368,8 +368,8 @@ async def test_skip_missing_dts(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) - # Check segment durations (not counting the elongated segment) + assert all(segments[i].sequence == i for i in range(len(segments))) + # Check segment durations (not counting the last segment) assert ( sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))]) >= len(segments) - 1 @@ -495,9 +495,9 @@ async def test_pts_out_of_order(hass): # Check number of segments assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) # Check sequence numbers - assert all([segments[i].sequence == i + 1 for i in range(len(segments))]) + assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all([s.duration == SEGMENT_DURATION for s in segments]) + assert all(s.duration == SEGMENT_DURATION for s in segments) assert len(decoded_stream.video_packets) == len(packets) assert len(decoded_stream.audio_packets) == 0 @@ -512,7 +512,7 @@ async def test_stream_stopped_while_decoding(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.add_provider(HLS_PROVIDER) py_av = MockPyAv() py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) @@ -539,7 +539,7 @@ async def test_update_stream_source(hass): worker_wake = threading.Event() stream = Stream(hass, STREAM_SOURCE) - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.add_provider(HLS_PROVIDER) # Note that keepalive is not set here. The stream is "restarted" even though # it is not stopping due to failure. @@ -572,14 +572,14 @@ async def test_update_stream_source(hass): assert last_stream_source == STREAM_SOURCE + "-updated-source" worker_wake.set() - # Ccleanup + # Cleanup stream.stop() async def test_worker_log(hass, caplog): """Test that the worker logs the url without username and password.""" stream = Stream(hass, "https://abcd:efgh@foo.bar") - stream.add_provider(STREAM_OUTPUT_FORMAT) + stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") segment_buffer = SegmentBuffer(stream.outputs) From 3e7729faf268cd18321019d25c0881b3c5a57688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 27 May 2021 06:04:05 +0200 Subject: [PATCH 005/750] Handle blank string in location name for mobile app (#51130) --- homeassistant/components/mobile_app/device_tracker.py | 4 +++- tests/components/mobile_app/test_device_tracker.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 1b006f69827..1deebf6b531 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -94,7 +94,9 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): @property def location_name(self): """Return a location name for the current location of the device.""" - return self._data.get(ATTR_LOCATION_NAME) + if location_name := self._data.get(ATTR_LOCATION_NAME): + return location_name + return None @property def name(self): diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 164b90a5290..b755a0a8d09 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -48,6 +48,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): "course": 6, "speed": 7, "vertical_accuracy": 8, + "location_name": "", }, }, ) @@ -82,7 +83,6 @@ async def test_restoring_location(hass, create_registrations, webhook_client): "course": 60, "speed": 70, "vertical_accuracy": 80, - "location_name": "bar", }, }, ) @@ -104,6 +104,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): assert state_1 is not state_2 assert state_2.name == "Test 1" + assert state_2.state == "not_home" assert state_2.attributes["source_type"] == "gps" assert state_2.attributes["latitude"] == 10 assert state_2.attributes["longitude"] == 20 From 877d3e38b482e26cb34230d9df3e10a00c23b295 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 27 May 2021 00:27:35 -0400 Subject: [PATCH 006/750] Fix zwave_js.set_value schema (#51114) * fix zwave_js.set_value schema * wrap all schemas in vol.Schema * readd removed assertions --- homeassistant/components/zwave_js/services.py | 106 ++++++++++-------- tests/components/zwave_js/test_services.py | 15 +++ 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 48719063376..c2ebe965fdd 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -67,22 +67,26 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, self.async_set_config_parameter, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( - vol.Coerce(int), cv.string - ), - vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), BITMASK_SCHEMA - ), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), - parameter_name_does_not_need_bitmask, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( + vol.Coerce(int), BITMASK_SCHEMA + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + parameter_name_does_not_need_bitmask, + ), ), ) @@ -90,21 +94,25 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, self.async_bulk_set_partial_config_parameters, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), - { - vol.Any( - vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) - }, - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any( + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), cv.string) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) @@ -125,21 +133,27 @@ class ZWaveServices: const.SERVICE_SET_VALUE, self.async_set_value, schema=vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), - vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str), - vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( - vol.Coerce(int), str - ), - vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 956361d3953..3c08c49a36f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -599,6 +599,7 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 5 @@ -619,3 +620,17 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): "value": 0, } assert args["value"] == 2 + + # Test missing device and entities keys + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + ATTR_WAIT_FOR_RESULT: True, + }, + blocking=True, + ) From e4e3dc7fab227df1581fddf0157ad99f0970d28e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 00:12:43 -0500 Subject: [PATCH 007/750] Fix Sonos TV source attribute (#51131) --- homeassistant/components/sonos/speaker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 7ce51176a88..e827b35b16d 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -819,7 +819,9 @@ class SonosSpeaker: if variables and "transport_state" in variables: self.media.play_mode = variables["current_play_mode"] - track_uri = variables["enqueued_transport_uri"] + track_uri = ( + variables["enqueued_transport_uri"] or variables["current_track_uri"] + ) music_source = self.soco.music_source_from_uri(track_uri) else: self.media.play_mode = self.soco.play_mode From fdfb84e8e2e5fbc67d3c9c597034c4cdfa78259f Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 27 May 2021 08:13:10 +0200 Subject: [PATCH 008/750] Upgrade pysonos to 0.0.50 (#51125) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/media_player.py | 10 ++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 7bd9efeda16..e42937d3889 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.49"], + "requirements": ["pysonos==0.0.50"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9ca4b21425b..0200ae11aa8 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -14,6 +14,7 @@ from pysonos.core import ( PLAY_MODES, ) from pysonos.exceptions import SoCoException, SoCoUPnPException +from pysonos.plugins.sharelink import ShareLinkPlugin import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -522,10 +523,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_id = media_id[len(PLEX_URI_SCHEME) :] play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call] elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + share_link = ShareLinkPlugin(soco) if kwargs.get(ATTR_MEDIA_ENQUEUE): try: - if soco.is_service_uri(media_id): - soco.add_service_uri_to_queue(media_id) + if share_link.is_share_link(media_id): + share_link.add_share_link_to_queue(media_id) else: soco.add_uri_to_queue(media_id) except SoCoUPnPException: @@ -536,9 +538,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_id, ) else: - if soco.is_service_uri(media_id): + if share_link.is_share_link(media_id): soco.clear_queue() - soco.add_service_uri_to_queue(media_id) + share_link.add_share_link_to_queue(media_id) soco.play_from_queue(0) else: soco.play_uri(media_id) diff --git a/requirements_all.txt b/requirements_all.txt index 98774e9a9ef..2bd8f06a618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1756,7 +1756,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.49 +pysonos==0.0.50 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2aa89f3d53..c5b3a826ffc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -983,7 +983,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.49 +pysonos==0.0.50 # homeassistant.components.spc pyspcwebgw==0.4.0 From 8d365e8bf503933146a4a642f7ee0161a6287bea Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 27 May 2021 08:28:31 +0200 Subject: [PATCH 009/750] After merge, review. (#51139) --- homeassistant/components/modbus/sensor.py | 3 +-- tests/components/modbus/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 2cc7d9223dc..9f1e7572a58 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -52,8 +52,7 @@ async def async_setup_platform( hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append(ModbusRegisterSensor(hub, entry)) - if len(sensors) > 0: - async_add_entities(sensors) + async_add_entities(sensors) class ModbusRegisterSensor(BasePlatform, RestoreEntity, SensorEntity): diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index bb92a6972f5..3de6bd2c172 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -210,7 +210,7 @@ async def test_config_wrong_struct_sensor( expect_setup_to_fail=True, ) - assert error_message in "".join(caplog.messages) + assert error_message in caplog.text @pytest.mark.parametrize( From f0952d3ee891b3b98296e0361d63051b1fce24cf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 03:53:51 -0500 Subject: [PATCH 010/750] Fix Sonos media position with radio sources (#51137) --- homeassistant/components/sonos/speaker.py | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e827b35b16d..c1f1dbb9104 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -844,7 +844,8 @@ class SonosSpeaker: if music_source == MUSIC_SRC_RADIO: self.update_media_radio(variables) else: - self.update_media_music(update_position, track_info) + self.update_media_music(track_info) + self.update_media_position(update_position, track_info) self.write_entity_states() @@ -907,11 +908,25 @@ class SonosSpeaker: if fav.reference.get_uri() == media_info["uri"]: self.media.source_name = fav.title - def update_media_music(self, update_media_position: bool, track_info: dict) -> None: + def update_media_music(self, track_info: dict) -> None: + """Update state when playing music tracks.""" + self.media.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position")) # type: ignore + if playlist_position > 0: + self.media.queue_position = playlist_position - 1 + + def update_media_position( + self, update_media_position: bool, track_info: dict + ) -> None: """Update state when playing music tracks.""" self.media.duration = _timespan_secs(track_info.get("duration")) current_position = _timespan_secs(track_info.get("position")) + if self.media.duration == 0: + self.media.clear_position() + return + # player started reporting position? if current_position is not None and self.media.position is None: update_media_position = True @@ -935,9 +950,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - self.media.image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position")) # type: ignore - if playlist_position > 0: - self.media.queue_position = playlist_position - 1 From cede36d91c6dce4109791cce2de30a5f45830f08 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 27 May 2021 10:55:47 +0200 Subject: [PATCH 011/750] Followup PR for SIA integration (#51108) * Updates based on Martin's review * fix strings and cleaned up constants --- .../components/sia/alarm_control_panel.py | 62 +++++-------- homeassistant/components/sia/config_flow.py | 30 +++---- homeassistant/components/sia/const.py | 34 +++----- homeassistant/components/sia/hub.py | 24 +++-- homeassistant/components/sia/manifest.json | 2 +- homeassistant/components/sia/strings.json | 2 +- homeassistant/components/sia/utils.py | 87 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 114 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 9d5f62b02de..fe5b95b639e 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -2,18 +2,14 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from pysiaalarm import SIAEvent -from homeassistant.components.alarm_control_panel import ( - ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PORT, - CONF_ZONE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -21,8 +17,10 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -33,7 +31,6 @@ from .const import ( CONF_PING_INTERVAL, CONF_ZONES, DOMAIN, - SIA_ENTITY_ID_FORMAT, SIA_EVENT, SIA_NAME_FORMAT, SIA_UNIQUE_ID_FORMAT_ALARM, @@ -76,21 +73,17 @@ CODE_CONSEQUENCES: dict[str, StateType] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[..., None], -) -> bool: + async_add_entities: AddEntitiesCallback, +) -> None: """Set up SIA alarm_control_panel(s) from a config entry.""" async_add_entities( - [ - SIAAlarmControlPanel(entry, account_data, zone) - for account_data in entry.data[CONF_ACCOUNTS] - for zone in range( - 1, - entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] - + 1, - ) - ] + SIAAlarmControlPanel(entry, account_data, zone) + for account_data in entry.data[CONF_ACCOUNTS] + for zone in range( + 1, + entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + 1, + ) ) - return True class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): @@ -111,18 +104,7 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): self._account: str = self._account_data[CONF_ACCOUNT] self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] - self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format( - SIA_ENTITY_ID_FORMAT.format( - self._port, self._account, self._zone, DEVICE_CLASS_ALARM - ) - ) - - self._attr: dict[str, Any] = { - CONF_PORT: self._port, - CONF_ACCOUNT: self._account, - CONF_ZONE: self._zone, - CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)", - } + self._attr: dict[str, Any] = {} self._available: bool = True self._state: StateType = None @@ -134,16 +116,17 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): Overridden from Entity. - 1. start the event listener and add the callback to on_remove + 1. register the dispatcher and add the callback to on_remove 2. get previous state from storage 3. if previous state: restore 4. if previous state is unavailable: set _available to False and return 5. if available: create availability cb """ self.async_on_remove( - self.hass.bus.async_listen( - event_type=SIA_EVENT.format(self._port, self._account), - listener=self.async_handle_event, + async_dispatcher_connect( + self.hass, + SIA_EVENT.format(self._port, self._account), + self.async_handle_event, ) ) last_state = await self.async_get_last_state() @@ -162,14 +145,11 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): if self._cancel_availability_cb: self._cancel_availability_cb() - async def async_handle_event(self, event: Event) -> None: - """Listen to events for this port and account and update state and attributes. + async def async_handle_event(self, sia_event: SIAEvent) -> None: + """Listen to dispatcher events for this port and account and update state and attributes. If the port and account combo receives any message it means it is online and can therefore be set to available. """ - sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member - event.data - ) _LOGGER.debug("Received event: %s", sia_event) if int(sia_event.ri) == self._zone: self._attr.update(get_attr_from_sia_event(sia_event)) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index fe49ec65777..a9b49765c19 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -104,7 +105,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: ConfigType = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: ConfigType = None): + async def async_step_user(self, user_input: ConfigType = None) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -115,7 +116,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_step_add_account(self, user_input: ConfigType = None): + async def async_step_add_account(self, user_input: ConfigType = None) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -126,11 +127,11 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_handle_data_and_route(self, user_input: ConfigType): + async def async_handle_data_and_route(self, user_input: ConfigType) -> FlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) - if self._data and self._port_already_configured(): - return self.async_abort(reason="already_configured") + + self._async_abort_entries_match({CONF_PORT: self._data[CONF_PORT]}) if user_input[CONF_ADDITIONAL_ACCOUNTS]: return await self.async_step_add_account() @@ -163,13 +164,6 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS)) self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] - def _port_already_configured(self): - """See if we already have a SIA entry matching the port.""" - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_PORT] == self._data[CONF_PORT]: - return True - return False - class SIAOptionsFlowHandler(config_entries.OptionsFlow): """Handle SIA options.""" @@ -181,14 +175,15 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: ConfigType = None): + async def async_step_init(self, user_input: ConfigType = None) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] - if self.hub is not None and self.hub.sia_accounts is not None: - self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] - return await self.async_step_options() + assert self.hub is not None + assert self.hub.sia_accounts is not None + self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] + return await self.async_step_options() - async def async_step_options(self, user_input: ConfigType = None): + async def async_step_options(self, user_input: ConfigType = None) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: @@ -223,7 +218,6 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] if self.accounts_todo: return await self.async_step_options() - _LOGGER.warning("Updating SIA Options with %s", self.options) return self.async_create_entry(title="", data=self.options) @property diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index ceeaac75923..916cdb9621c 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -5,34 +5,24 @@ from homeassistant.components.alarm_control_panel import ( PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] +DOMAIN = "sia" + +ATTR_CODE = "last_code" +ATTR_ZONE = "zone" +ATTR_MESSAGE = "last_message" +ATTR_ID = "last_id" +ATTR_TIMESTAMP = "last_timestamp" + +TITLE = "SIA Alarm on port {}" CONF_ACCOUNT = "account" CONF_ACCOUNTS = "accounts" CONF_ADDITIONAL_ACCOUNTS = "additional_account" -CONF_PING_INTERVAL = "ping_interval" CONF_ENCRYPTION_KEY = "encryption_key" -CONF_ZONES = "zones" CONF_IGNORE_TIMESTAMPS = "ignore_timestamps" +CONF_PING_INTERVAL = "ping_interval" +CONF_ZONES = "zones" -DOMAIN = "sia" -TITLE = "SIA Alarm on port {}" -SIA_EVENT = "sia_event_{}_{}" SIA_NAME_FORMAT = "{} - {} - zone {} - {}" -SIA_NAME_FORMAT_HUB = "{} - {} - {}" -SIA_ENTITY_ID_FORMAT = "{}_{}_{}_{}" -SIA_ENTITY_ID_FORMAT_HUB = "{}_{}_{}" SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" -SIA_UNIQUE_ID_FORMAT = "{}_{}_{}_{}" -HUB_SENSOR_NAME = "last_heartbeat" -HUB_ZONE = 0 -PING_INTERVAL_MARGIN = 30 -DEFAULT_TIMEBAND = (80, 40) -IGNORED_TIMEBAND = (3600, 1800) - -EVENT_CODE = "last_code" -EVENT_ACCOUNT = "account" -EVENT_ZONE = "zone" -EVENT_PORT = "port" -EVENT_MESSAGE = "last_message" -EVENT_ID = "last_id" -EVENT_TIMESTAMP = "last_timestamp" +SIA_EVENT = "sia_event_{}_{}" diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index e5dc7b85ed8..387c2273606 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -9,8 +9,9 @@ from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEve from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, EventOrigin, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ACCOUNT, @@ -18,16 +19,19 @@ from .const import ( CONF_ENCRYPTION_KEY, CONF_IGNORE_TIMESTAMPS, CONF_ZONES, - DEFAULT_TIMEBAND, DOMAIN, - IGNORED_TIMEBAND, PLATFORMS, SIA_EVENT, ) +from .utils import get_event_data_from_sia_event _LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEBAND = (80, 40) +IGNORED_TIMEBAND = (3600, 1800) + + class SIAHub: """Class for SIA Hubs.""" @@ -39,7 +43,7 @@ class SIAHub: """Create the SIAHub.""" self._hass: HomeAssistant = hass self._entry: ConfigEntry = entry - self._port: int = int(entry.data[CONF_PORT]) + self._port: int = entry.data[CONF_PORT] self._title: str = entry.title self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS]) self._protocol: str = entry.data[CONF_PROTOCOL] @@ -69,21 +73,23 @@ class SIAHub: await self.sia_client.stop() async def async_create_and_fire_event(self, event: SIAEvent) -> None: - """Create a event on HA's bus, with the data from the SIAEvent. + """Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent. The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms. """ _LOGGER.debug( - "Adding event to bus for code %s for port %s and account %s", + "Adding event to dispatch and bus for code %s for port %s and account %s", event.code, self._port, event.account, ) + async_dispatcher_send( + self._hass, SIA_EVENT.format(self._port, event.account), event + ) self._hass.bus.async_fire( event_type=SIA_EVENT.format(self._port, event.account), - event_data=event.to_dict(encode_json=True), - origin=EventOrigin.remote, + event_data=get_event_data_from_sia_event(event), ) def update_accounts(self): @@ -115,7 +121,7 @@ class SIAHub: options = dict(self._entry.options) for acc in self._accounts: acc_id = acc[CONF_ACCOUNT] - if acc_id in options[CONF_ACCOUNTS].keys(): + if acc_id in options[CONF_ACCOUNTS]: acc[CONF_IGNORE_TIMESTAMPS] = options[CONF_ACCOUNTS][acc_id][ CONF_IGNORE_TIMESTAMPS ] diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index 67c2a0e91a1..eaeb4547167 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -3,7 +3,7 @@ "name": "SIA Alarm Systems", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sia", - "requirements": ["pysiaalarm==3.0.0b12"], + "requirements": ["pysiaalarm==3.0.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "local_push" } diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index b091fdd341d..f837d41056a 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -27,7 +27,7 @@ }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", - "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 9b02025aa8d..08e0fce8ab2 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,19 +6,9 @@ from typing import Any from pysiaalarm import SIAEvent -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE -from .const import ( - EVENT_ACCOUNT, - EVENT_CODE, - EVENT_ID, - EVENT_MESSAGE, - EVENT_TIMESTAMP, - EVENT_ZONE, - HUB_SENSOR_NAME, - HUB_ZONE, - PING_INTERVAL_MARGIN, -) +PING_INTERVAL_MARGIN = 30 def get_unavailability_interval(ping: int) -> float: @@ -26,32 +16,55 @@ def get_unavailability_interval(ping: int) -> float: return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() -def get_name(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id and name according to the variables.""" - if zone == HUB_ZONE: - return f"{port} - {account} - {'Last Heartbeat' if entity_type == DEVICE_CLASS_TIMESTAMP else 'Power'}" - return f"{port} - {account} - zone {zone} - {entity_type}" - - -def get_entity_id(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id according to the variables.""" - if zone == HUB_ZONE: - return f"{port}_{account}_{HUB_SENSOR_NAME if entity_type == DEVICE_CLASS_TIMESTAMP else entity_type}" - return f"{port}_{account}_{zone}_{entity_type}" - - -def get_unique_id(entry_id: str, account: str, zone: int, domain: str) -> str: - """Return the unique id.""" - return f"{entry_id}_{account}_{zone}_{domain}" - - def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create the attributes dict from a SIAEvent.""" return { - EVENT_ACCOUNT: event.account, - EVENT_ZONE: event.ri, - EVENT_CODE: event.code, - EVENT_MESSAGE: event.message, - EVENT_ID: event.id, - EVENT_TIMESTAMP: event.timestamp, + ATTR_ZONE: event.ri, + ATTR_CODE: event.code, + ATTR_MESSAGE: event.message, + ATTR_ID: event.id, + ATTR_TIMESTAMP: event.timestamp.isoformat(), + } + + +def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: + """Create a dict from the SIA Event for the HA Event.""" + return { + "message_type": event.message_type, + "receiver": event.receiver, + "line": event.line, + "account": event.account, + "sequence": event.sequence, + "content": event.content, + "ti": event.ti, + "id": event.id, + "ri": event.ri, + "code": event.code, + "message": event.message, + "x_data": event.x_data, + "timestamp": event.timestamp.isoformat(), + "event_qualifier": event.qualifier, + "event_type": event.event_type, + "partition": event.partition, + "extended_data": [ + { + "identifier": xd.identifier, + "name": xd.name, + "description": xd.description, + "length": xd.length, + "characters": xd.characters, + "value": xd.value, + } + for xd in event.extended_data + ] + if event.extended_data is not None + else None, + "sia_code": { + "code": event.sia_code.code, + "type": event.sia_code.type, + "description": event.sia_code.description, + "concerns": event.sia_code.concerns, + } + if event.sia_code is not None + else None, } diff --git a/requirements_all.txt b/requirements_all.txt index 2bd8f06a618..29a490cfc43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5b3a826ffc..1caa6ba7b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pyserial-asyncio==0.5 pyserial==3.5 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 From 65f2fe9c011af7657591c432b073eef54e0c28cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Thu, 27 May 2021 12:53:14 +0200 Subject: [PATCH 012/750] Bump pysma version to 0.5.0 (#51098) * Use new get_sensors method * Update pysma requirement * Update primary codeowner * Update device_info handling * Fix LEGACY_MAP * Updated tests * Fix pysma references * Fix pylint raise-missing-from * Better import of Sensors * Remove software version related changes * Revert codeowners change --- homeassistant/components/sma/__init__.py | 35 ++++++++++++---------- homeassistant/components/sma/manifest.json | 2 +- homeassistant/components/sma/sensor.py | 6 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sma/conftest.py | 7 ++++- tests/components/sma/test_sensor.py | 2 +- 7 files changed, 33 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index ef948440a17..d8fcbbf8099 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -40,7 +40,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list[str]: +def _parse_legacy_options( + entry: ConfigEntry, sensor_def: pysma.sensor.Sensors +) -> list[str]: """Parse legacy configuration options. This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options @@ -50,7 +52,9 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list # Add sensors from the custom config sensor_def.add( [ - pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH)) + pysma.sensor.Sensor( + o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH) + ) for n, o in entry.data.get(CONF_CUSTOM).items() ] ) @@ -74,9 +78,9 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list # Find and replace sensors removed from pysma # This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids for sensor in config_sensors.copy(): - if sensor in pysma.LEGACY_MAP: + if sensor in pysma.const.LEGACY_MAP: config_sensors.remove(sensor) - config_sensors.append(pysma.LEGACY_MAP[sensor]["new_sensor"]) + config_sensors.append(pysma.const.LEGACY_MAP[sensor]["new_sensor"]) # Only sensors from config should be enabled for sensor in sensor_def: @@ -88,7 +92,7 @@ def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> list def _migrate_old_unique_ids( hass: HomeAssistant, entry: ConfigEntry, - sensor_def: pysma.Sensors, + sensor_def: pysma.sensor.Sensors, config_sensors: list[str], ) -> None: """Migrate legacy sensor entity_id format to new format.""" @@ -96,16 +100,16 @@ def _migrate_old_unique_ids( # Create list of all possible sensor names possible_sensors = set( - config_sensors + [s.name for s in sensor_def] + list(pysma.LEGACY_MAP) + config_sensors + [s.name for s in sensor_def] + list(pysma.const.LEGACY_MAP) ) for sensor in possible_sensors: if sensor in sensor_def: pysma_sensor = sensor_def[sensor] original_key = pysma_sensor.key - elif sensor in pysma.LEGACY_MAP: + elif sensor in pysma.const.LEGACY_MAP: # If sensor was removed from pysma we will remap it to the new sensor - legacy_sensor = pysma.LEGACY_MAP[sensor] + legacy_sensor = pysma.const.LEGACY_MAP[sensor] pysma_sensor = sensor_def[legacy_sensor["new_sensor"]] original_key = legacy_sensor["old_key"] else: @@ -127,13 +131,6 @@ def _migrate_old_unique_ids( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up sma from a config entry.""" - # Init all default sensors - sensor_def = pysma.Sensors() - - if entry.source == SOURCE_IMPORT: - config_sensors = _parse_legacy_options(entry, sensor_def) - _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors) - # Init the SMA interface protocol = "https" if entry.data[CONF_SSL] else "http" url = f"{protocol}://{entry.data[CONF_HOST]}" @@ -144,6 +141,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) + # Get all device sensors + sensor_def = await sma.get_sensors() + + # Parse legacy options if initial setup was done from yaml + if entry.source == SOURCE_IMPORT: + config_sensors = _parse_legacy_options(entry, sensor_def) + _migrate_old_unique_ids(hass, entry, sensor_def, config_sensors) + # Define the coordinator async def async_update_data(): """Update the used SMA sensors.""" diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 8add6f830e8..66b845ac39f 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.4.3"], + "requirements": ["pysma==0.5.0"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 04bfb7644a3..2ef999a6579 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -46,8 +46,8 @@ _LOGGER = logging.getLogger(__name__) def _check_sensor_schema(conf: dict[str, Any]) -> dict[str, Any]: """Check sensors and attributes are valid.""" try: - valid = [s.name for s in pysma.Sensors()] - valid += pysma.LEGACY_MAP.keys() + valid = [s.name for s in pysma.sensor.Sensors()] + valid += pysma.const.LEGACY_MAP.keys() except (ImportError, AttributeError): return conf @@ -147,7 +147,7 @@ class SMAsensor(CoordinatorEntity, SensorEntity): coordinator: DataUpdateCoordinator, config_entry_unique_id: str, device_info: dict[str, Any], - pysma_sensor: pysma.Sensor, + pysma_sensor: pysma.sensor.Sensor, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) diff --git a/requirements_all.txt b/requirements_all.txt index 29a490cfc43..989829b0e61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.4.3 +pysma==0.5.0 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1caa6ba7b8e..551de00ccc3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.4.3 +pysma==0.5.0 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index 9ec9e1f5a11..80d9b38e28b 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -1,6 +1,9 @@ """Fixtures for sma tests.""" from unittest.mock import patch +from pysma.const import DEVCLASS_INVERTER +from pysma.definitions import sensor_map +from pysma.sensor import Sensors import pytest from homeassistant import config_entries @@ -28,7 +31,9 @@ async def init_integration(hass, mock_config_entry): """Create a fake SMA Config Entry.""" mock_config_entry.add_to_hass(hass) - with patch("pysma.SMA.read"): + with patch("pysma.SMA.read"), patch( + "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[DEVCLASS_INVERTER]) + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() return mock_config_entry diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index b86533a11df..129af154924 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -10,7 +10,7 @@ from . import MOCK_CUSTOM_SENSOR async def test_sensors(hass, init_integration): """Test states of the sensors.""" - state = hass.states.get("sensor.current_consumption") + state = hass.states.get("sensor.grid_power") assert state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT From 69e454fd49319820c77f335c43e85e7841ba37bd Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 27 May 2021 13:52:05 +0200 Subject: [PATCH 013/750] Add missing function signature (#51153) --- homeassistant/components/asuswrt/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 0912869abb7..e929ae80e26 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -419,7 +419,7 @@ class AsusWrtRouter: return self._api -async def _get_nvram_info(api: AsusWrt, info_type): +async def _get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: """Get AsusWrt router info from nvram.""" info = {} try: From eb2b60434c0166b12c9f36208aa7cd03803bec93 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 May 2021 14:04:40 +0200 Subject: [PATCH 014/750] Clean up Local IP integration (#51126) --- .strict-typing | 1 + homeassistant/components/local_ip/__init__.py | 4 +- .../components/local_ip/config_flow.py | 13 ++++-- homeassistant/components/local_ip/sensor.py | 43 +++++++------------ mypy.ini | 11 +++++ 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/.strict-typing b/.strict-typing index 00bc3447d22..3d777a11acf 100644 --- a/.strict-typing +++ b/.strict-typing @@ -41,6 +41,7 @@ homeassistant.components.integration.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.light.* +homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.mailbox.* homeassistant.components.media_player.* diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index c4e8c541e4a..e97e5da7d49 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -8,12 +8,12 @@ from .const import DOMAIN, PLATFORMS CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up local_ip from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py index 2bc994c4dca..27bd5340d40 100644 --- a/homeassistant/components/local_ip/config_flow.py +++ b/homeassistant/components/local_ip/config_flow.py @@ -1,18 +1,23 @@ """Config flow for local_ip.""" +from __future__ import annotations -from homeassistant import config_entries +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SimpleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for local_ip.""" VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index 1d2cce72105..c7bc53caa69 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -1,46 +1,35 @@ """Sensor platform for local_ip.""" from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import get_local_ip from .const import DOMAIN, SENSOR -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the platform from config_entry.""" - name = config_entry.data.get(CONF_NAME) or DOMAIN + name = entry.data.get(CONF_NAME) or DOMAIN async_add_entities([IPSensor(name)], True) class IPSensor(SensorEntity): """A simple sensor.""" - def __init__(self, name): + _attr_unique_id = SENSOR + _attr_icon = "mdi:ip" + + def __init__(self, name: str) -> None: """Initialize the sensor.""" - self._state = None - self._name = name + self._attr_name = name - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return SENSOR - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:ip" - - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" - self._state = get_local_ip() + self._attr_state = get_local_ip() diff --git a/mypy.ini b/mypy.ini index c65f28336ff..231f7f40384 100644 --- a/mypy.ini +++ b/mypy.ini @@ -462,6 +462,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.local_ip.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lock.*] check_untyped_defs = true disallow_incomplete_defs = true From d9eb1d85a246329119d182de8da897d3210e71ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 May 2021 14:10:28 +0200 Subject: [PATCH 015/750] Clean up DNS IP integration (#51143) * Clean up DNS IP integration * Commit missing change oops --- .strict-typing | 1 + homeassistant/components/dnsip/sensor.py | 52 ++++++++++-------------- mypy.ini | 11 +++++ 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/.strict-typing b/.strict-typing index 3d777a11acf..e1c0d905621 100644 --- a/.strict-typing +++ b/.strict-typing @@ -24,6 +24,7 @@ homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* +homeassistant.components.dnsip.* homeassistant.components.dunehd.* homeassistant.components.elgato.* homeassistant.components.fitbit.* diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 01d6e2f4f2a..2fb0e30da90 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -1,4 +1,6 @@ """Get your own public IP address or that of any host.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -8,7 +10,10 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -36,57 +41,44 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_devices: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the DNS IP sensor.""" hostname = config[CONF_HOSTNAME] name = config.get(CONF_NAME) - if not name: - if hostname == DEFAULT_HOSTNAME: - name = DEFAULT_NAME - else: - name = hostname ipv6 = config[CONF_IPV6] - if ipv6: - resolver = config[CONF_RESOLVER_IPV6] - else: - resolver = config[CONF_RESOLVER] - async_add_devices([WanIpSensor(hass, name, hostname, resolver, ipv6)], True) + if not name: + name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname + resolver = config[CONF_RESOLVER_IPV6] if ipv6 else config[CONF_RESOLVER] + + async_add_devices([WanIpSensor(name, hostname, resolver, ipv6)], True) class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" - def __init__(self, hass, name, hostname, resolver, ipv6): + def __init__(self, name: str, hostname: str, resolver: str, ipv6: bool) -> None: """Initialize the DNS IP sensor.""" - - self.hass = hass - self._name = name + self._attr_name = name self.hostname = hostname self.resolver = aiodns.DNSResolver() self.resolver.nameservers = [resolver] self.querytype = "AAAA" if ipv6 else "A" - self._state = None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the current DNS IP address for hostname.""" - return self._state - - async def async_update(self): + async def async_update(self) -> None: """Get the current DNS IP address for hostname.""" - try: response = await self.resolver.query(self.hostname, self.querytype) except DNSError as err: _LOGGER.warning("Exception while resolving host: %s", err) response = None + if response: - self._state = response[0].host + self._attr_state = response[0].host else: - self._state = None + self._attr_state = None diff --git a/mypy.ini b/mypy.ini index 231f7f40384..8de49a59259 100644 --- a/mypy.ini +++ b/mypy.ini @@ -275,6 +275,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.dnsip.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.dunehd.*] check_untyped_defs = true disallow_incomplete_defs = true From 701c4ee624bfc593cd41df9efabb84291ad32415 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 27 May 2021 15:35:17 +0200 Subject: [PATCH 016/750] Update sia tests (#51151) --- tests/components/sia/test_config_flow.py | 154 ++++++++++++++--------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 204518c1e5a..9679f4949e8 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -104,29 +104,6 @@ ADDITIONAL_OPTIONS = { } } -BASIC_CONFIG_ENTRY = MockConfigEntry( - domain=DOMAIN, - data=BASE_OUT["data"], - options=BASE_OUT["options"], - title="SIA Alarm on port 7777", - entry_id=BASIS_CONFIG_ENTRY_ID, - version=1, -) -ADDITIONAL_CONFIG_ENTRY = MockConfigEntry( - domain=DOMAIN, - data=ADDITIONAL_OUT["data"], - options=ADDITIONAL_OUT["options"], - title="SIA Alarm on port 7777", - entry_id=ADDITIONAL_CONFIG_ENTRY_ID, - version=1, -) - - -@pytest.fixture(params=[False, True], ids=["user", "add_account"]) -def additional(request) -> bool: - """Return True or False for the additional or base test.""" - return request.param - @pytest.fixture async def flow_at_user_step(hass): @@ -140,7 +117,7 @@ async def flow_at_user_step(hass): @pytest.fixture async def entry_with_basic_config(hass, flow_at_user_step): """Return a entry with a basic config.""" - with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + with patch("homeassistant.components.sia.async_setup_entry", return_value=True): return await hass.config_entries.flow.async_configure( flow_at_user_step["flow_id"], BASIC_CONFIG ) @@ -157,7 +134,7 @@ async def flow_at_add_account_step(hass, flow_at_user_step): @pytest.fixture async def entry_with_additional_account_config(hass, flow_at_add_account_step): """Return a entry with a two account config.""" - with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + with patch("homeassistant.components.sia.async_setup_entry", return_value=True): return await hass.config_entries.flow.async_configure( flow_at_add_account_step["flow_id"], ADDITIONAL_ACCOUNT ) @@ -171,20 +148,20 @@ async def setup_sia(hass, config_entry: MockConfigEntry): await hass.async_block_till_done() -async def test_form_start( - hass, flow_at_user_step, flow_at_add_account_step, additional -): - """Start the form and check if you get the right id and schema.""" - if additional: - assert flow_at_add_account_step["step_id"] == "add_account" - assert flow_at_add_account_step["errors"] is None - assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA - return +async def test_form_start_user(hass, flow_at_user_step): + """Start the form and check if you get the right id and schema for the user step.""" assert flow_at_user_step["step_id"] == "user" assert flow_at_user_step["errors"] is None assert flow_at_user_step["data_schema"] == HUB_SCHEMA +async def test_form_start_account(hass, flow_at_add_account_step): + """Start the form and check if you get the right id and schema for the additional account step.""" + assert flow_at_add_account_step["step_id"] == "add_account" + assert flow_at_add_account_step["errors"] is None + assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA + + async def test_create(hass, entry_with_basic_config): """Test we create a entry through the form.""" assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -211,9 +188,17 @@ async def test_create_additional_account(hass, entry_with_additional_account_con assert entry_with_additional_account_config["options"] == ADDITIONAL_OUT["options"] -async def test_abort_form(hass, entry_with_basic_config): +async def test_abort_form(hass): """Test aborting a config that already exists.""" - assert entry_with_basic_config["data"][CONF_PORT] == BASIC_CONFIG[CONF_PORT] + config_entry = MockConfigEntry( + domain=DOMAIN, + data=BASE_OUT["data"], + options=BASE_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=BASIS_CONFIG_ENTRY_ID, + version=1, + ) + await setup_sia(hass, config_entry) start_another_flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -235,54 +220,97 @@ async def test_abort_form(hass, entry_with_basic_config): ("zones", 0, "invalid_zones"), ], ) -async def test_validation_errors( +async def test_validation_errors_user( hass, flow_at_user_step, - additional, field, value, error, ): - """Test we handle the different invalid inputs, both in the user and add_account flow.""" + """Test we handle the different invalid inputs, in the user flow.""" config = BASIC_CONFIG.copy() flow_id = flow_at_user_step["flow_id"] - if additional: - flow_at_add_account_step = await hass.config_entries.flow.async_configure( - flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL - ) - config = ADDITIONAL_ACCOUNT.copy() - flow_id = flow_at_add_account_step["flow_id"] - config[field] = value result_err = await hass.config_entries.flow.async_configure(flow_id, config) assert result_err["type"] == "form" assert result_err["errors"] == {"base": error} -async def test_unknown(hass, flow_at_user_step, additional): +@pytest.mark.parametrize( + "field, value, error", + [ + ("encryption_key", "AAAAAAAAAAAAAZZZ", "invalid_key_format"), + ("encryption_key", "AAAAAAAAAAAAA", "invalid_key_length"), + ("account", "ZZZ", "invalid_account_format"), + ("account", "A", "invalid_account_length"), + ("ping_interval", 1500, "invalid_ping"), + ("zones", 0, "invalid_zones"), + ], +) +async def test_validation_errors_account( + hass, + flow_at_user_step, + field, + value, + error, +): + """Test we handle the different invalid inputs, in the add_account flow.""" + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + config = ADDITIONAL_ACCOUNT.copy() + flow_id = flow_at_add_account_step["flow_id"] + config[field] = value + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err["type"] == "form" + assert result_err["errors"] == {"base": error} + + +async def test_unknown_user(hass, flow_at_user_step): """Test unknown exceptions.""" flow_id = flow_at_user_step["flow_id"] - if additional: - flow_at_add_account_step = await hass.config_entries.flow.async_configure( - flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL - ) - flow_id = flow_at_add_account_step["flow_id"] with patch( "pysiaalarm.SIAAccount.validate_account", side_effect=Exception, ): - config = ADDITIONAL_ACCOUNT if additional else BASIC_CONFIG + config = BASIC_CONFIG result_err = await hass.config_entries.flow.async_configure(flow_id, config) assert result_err - assert result_err["step_id"] == "add_account" if additional else "user" + assert result_err["step_id"] == "user" assert result_err["errors"] == {"base": "unknown"} - assert result_err["data_schema"] == ACCOUNT_SCHEMA if additional else HUB_SCHEMA + assert result_err["data_schema"] == HUB_SCHEMA + + +async def test_unknown_account(hass, flow_at_user_step): + """Test unknown exceptions.""" + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + flow_id = flow_at_add_account_step["flow_id"] + with patch( + "pysiaalarm.SIAAccount.validate_account", + side_effect=Exception, + ): + config = ADDITIONAL_ACCOUNT + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err + assert result_err["step_id"] == "add_account" + assert result_err["errors"] == {"base": "unknown"} + assert result_err["data_schema"] == ACCOUNT_SCHEMA async def test_options_basic(hass): """Test options flow for single account.""" - await setup_sia(hass, BASIC_CONFIG_ENTRY) - result = await hass.config_entries.options.async_init(BASIC_CONFIG_ENTRY.entry_id) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=BASE_OUT["data"], + options=BASE_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=BASIS_CONFIG_ENTRY_ID, + version=1, + ) + await setup_sia(hass, config_entry) + 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"] == "options" assert result["last_step"] @@ -298,10 +326,16 @@ async def test_options_basic(hass): async def test_options_additional(hass): """Test options flow for single account.""" - await setup_sia(hass, ADDITIONAL_CONFIG_ENTRY) - result = await hass.config_entries.options.async_init( - ADDITIONAL_CONFIG_ENTRY.entry_id + config_entry = MockConfigEntry( + domain=DOMAIN, + data=ADDITIONAL_OUT["data"], + options=ADDITIONAL_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=ADDITIONAL_CONFIG_ENTRY_ID, + version=1, ) + await setup_sia(hass, config_entry) + 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"] == "options" assert not result["last_step"] From c0656878dbc0132ddc3487701ff7473319a4e73b Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 27 May 2021 09:56:20 -0400 Subject: [PATCH 017/750] Normalize async_setup_entry (#51161) --- homeassistant/components/aemet/__init__.py | 30 +++++++-------- homeassistant/components/almond/__init__.py | 10 ++--- .../components/arcam_fmj/__init__.py | 4 +- homeassistant/components/buienradar/camera.py | 4 +- .../components/climacell/__init__.py | 30 +++++++-------- homeassistant/components/climate/__init__.py | 3 +- homeassistant/components/dsmr/__init__.py | 4 +- homeassistant/components/foscam/__init__.py | 26 ++++++------- .../components/google_travel_time/__init__.py | 14 +++---- .../components/gpslogger/device_tracker.py | 5 ++- .../components/home_plus_control/__init__.py | 12 +++--- homeassistant/components/homekit/__init__.py | 2 +- .../components/huawei_lte/__init__.py | 37 +++++++++---------- homeassistant/components/hyperion/__init__.py | 28 +++++++------- .../components/keenetic_ndms2/__init__.py | 26 ++++++------- homeassistant/components/kraken/__init__.py | 10 ++--- .../components/meteoclimatic/__init__.py | 6 +-- .../components/meteoclimatic/weather.py | 4 +- .../components/minecraft_server/__init__.py | 12 +++--- homeassistant/components/onewire/__init__.py | 10 ++--- .../components/openweathermap/__init__.py | 36 +++++++++--------- .../pvpc_hourly_pricing/__init__.py | 8 ++-- .../components/srp_energy/__init__.py | 4 +- homeassistant/components/starline/__init__.py | 24 ++++++------ homeassistant/components/tplink/__init__.py | 6 +-- .../components/traccar/device_tracker.py | 5 ++- homeassistant/components/twinkly/__init__.py | 12 +++--- homeassistant/components/upcloud/__init__.py | 31 +++++++--------- homeassistant/components/upnp/__init__.py | 26 ++++++------- homeassistant/components/vera/__init__.py | 32 ++++++++-------- homeassistant/components/vizio/__init__.py | 6 +-- .../components/waze_travel_time/__init__.py | 14 +++---- homeassistant/components/wiffi/__init__.py | 28 +++++++------- .../components/xbox/binary_sensor.py | 9 +++-- tests/components/wallbox/test_init.py | 14 +++---- 35 files changed, 259 insertions(+), 273 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 879f59fa2fc..e958195b215 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -19,13 +19,13 @@ from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up AEMET OpenData as config entry.""" - name = config_entry.data[CONF_NAME] - api_key = config_entry.data[CONF_API_KEY] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] - station_updates = config_entry.options.get(CONF_STATION_UPDATES, True) + name = entry.data[CONF_NAME] + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + station_updates = entry.options.get(CONF_STATION_UPDATES, True) aemet = AEMET(api_key) weather_coordinator = WeatherUpdateCoordinator( @@ -35,30 +35,28 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): await weather_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data[DOMAIN][entry.entry_id] = { ENTRY_NAME: name, ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - config_entry.async_on_unload(config_entry.add_update_listener(async_update_options)) + entry.async_on_unload(entry.add_update_listener(async_update_options)) return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 0c012788b0e..b8e13cc1dee 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -11,9 +11,9 @@ import async_timeout from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI import voluptuous as vol -from homeassistant import config_entries from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import conversation +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -94,14 +94,14 @@ async def async_setup(hass, config): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, + context={"source": SOURCE_IMPORT}, data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, ) ) return True -async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) @@ -150,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEnt async def _configure_almond_for_ha( - hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI + hass: HomeAssistant, entry: ConfigEntry, api: WebAlmondAPI ): """Configure Almond to connect to HA.""" try: @@ -248,7 +248,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): """Almond conversation agent.""" def __init__( - self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry + self, hass: HomeAssistant, api: WebAlmondAPI, entry: ConfigEntry ) -> None: """Initialize the agent.""" self.hass = hass diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index e1dfac09d76..3b49008cb5f 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -7,7 +7,7 @@ from arcam.fmj import ConnectionFailed from arcam.fmj.client import Client import async_timeout -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up config entry.""" entries = hass.data[DOMAIN_DATA_ENTRIES] tasks = hass.data[DOMAIN_DATA_TASKS] diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 1a2d6d4d0be..059cd79d522 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -11,10 +11,10 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from .const import ( @@ -56,7 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up buienradar radar-loop camera component.""" config = entry.data diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 85a23ef10a9..09909ae4e3a 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -109,19 +109,19 @@ def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> tim return interval -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) params = {} # If config entry options not set up, set them up - if not config_entry.options: + if not entry.options: params["options"] = { CONF_TIMESTEP: DEFAULT_TIMESTEP, } else: # Use valid timestep if it's invalid - timestep = config_entry.options[CONF_TIMESTEP] + timestep = entry.options[CONF_TIMESTEP] if timestep not in (1, 5, 15, 30): if timestep <= 2: timestep = 1 @@ -131,38 +131,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b timestep = 15 else: timestep = 30 - new_options = config_entry.options.copy() + new_options = entry.options.copy() new_options[CONF_TIMESTEP] = timestep params["options"] = new_options # Add API version if not found - if CONF_API_VERSION not in config_entry.data: - new_data = config_entry.data.copy() + if CONF_API_VERSION not in entry.data: + new_data = entry.data.copy() new_data[CONF_API_VERSION] = 3 params["data"] = new_data if params: - hass.config_entries.async_update_entry(config_entry, **params) + hass.config_entries.async_update_entry(entry, **params) - api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 + api_class = ClimaCellV3 if entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 api = api_class( - config_entry.data[CONF_API_KEY], - config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + entry.data[CONF_API_KEY], + entry.data.get(CONF_LATITUDE, hass.config.latitude), + entry.data.get(CONF_LONGITUDE, hass.config.longitude), session=async_get_clientsession(hass), ) coordinator = ClimaCellDataUpdateCoordinator( hass, - config_entry, + entry, api, - _set_update_interval(hass, config_entry), + _set_update_interval(hass, entry), ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 30842f1fe23..0369933dd0a 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -9,6 +9,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_TENTHS, @@ -157,7 +158,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 3af620df19c..ba876111a4e 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -40,6 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 308b1a3cc9f..148768d6289 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -36,26 +36,26 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def async_migrate_entry(hass, config_entry: ConfigEntry): +async def async_migrate_entry(hass, entry: ConfigEntry): """Migrate old entry.""" - LOGGER.debug("Migrating from version %s", config_entry.version) + LOGGER.debug("Migrating from version %s", entry.version) - if config_entry.version == 1: + if entry.version == 1: # Change unique id @callback def update_unique_id(entry): - return {"new_unique_id": config_entry.entry_id} + return {"new_unique_id": entry.entry_id} - await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + await async_migrate_entries(hass, entry.entry_id, update_unique_id) - config_entry.unique_id = None + entry.unique_id = None # Get RTSP port from the camera or use the fallback one and store it in data camera = FoscamCamera( - config_entry.data[CONF_HOST], - config_entry.data[CONF_PORT], - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], verbose=False, ) @@ -66,11 +66,11 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): if ret != 0: rtsp_port = response.get("rtspPort") or response.get("mediaPort") - config_entry.data = {**config_entry.data, CONF_RTSP_PORT: rtsp_port} + entry.data = {**entry.data, CONF_RTSP_PORT: rtsp_port} # Change entry version - config_entry.version = 2 + entry.version = 2 - LOGGER.info("Migration to version %s successful", config_entry.version) + LOGGER.info("Migration to version %s successful", entry.version) return True diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index bad6edd119e..231a2db22e2 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -12,18 +12,16 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Google Maps Travel Time from a config entry.""" - if config_entry.unique_id is not None: - hass.config_entries.async_update_entry(config_entry, unique_id=None) + if entry.unique_id is not None: + hass.config_entries.async_update_entry(entry, unique_id=None) ent_reg = async_get(hass) - for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id): - ent_reg.async_update_entity( - entity.entity_id, new_unique_id=config_entry.entry_id - ) + for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): + ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 5bce10ab088..2493054473a 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,6 +1,7 @@ """Support for the GPSLogger device tracking.""" from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, @@ -22,7 +23,9 @@ from .const import ( ) -async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): """Configure a dispatcher connection based on a config entry.""" @callback diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 176dc2fbd02..954203e9b10 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -66,22 +66,20 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Legrand Home+ Control from a config entry.""" - hass_entry_data = hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) + hass_entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # Retrieve the registered implementation implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, config_entry + hass, entry ) ) # Using an aiohttp-based API lib, so rely on async framework # Add the API object to the domain's data in HA - api = hass_entry_data[API] = HomePlusControlAsyncApi( - hass, config_entry, implementation - ) + api = hass_entry_data[API] = HomePlusControlAsyncApi(hass, entry, implementation) # Set of entity unique identifiers of this integration uids = hass_entry_data[ENTITY_UIDS] = set() @@ -143,7 +141,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Continue setting up the platforms.""" await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(config_entry, platform) + hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS ] ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 87742104b86..77a9ae27400 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -297,7 +297,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" dismiss_setup_message(hass, entry.entry_id) homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ebb54ab75c6..7503c1d5e71 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -304,9 +304,9 @@ class HuaweiLteData: routers: dict[str, Router] = attr.ib(init=False, factory=dict) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" - url = config_entry.data[CONF_URL] + url = entry.data[CONF_URL] # Override settings from YAML config, but only if they're changed in it # Old values are stored as *_from_yaml in the config entry @@ -317,30 +317,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b for key in CONF_USERNAME, CONF_PASSWORD: if key in yaml_config: value = yaml_config[key] - if value != config_entry.data.get(f"{key}_from_yaml"): + if value != entry.data.get(f"{key}_from_yaml"): new_data[f"{key}_from_yaml"] = value new_data[key] = value # Options new_options = {} yaml_recipient = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_RECIPIENT) - if yaml_recipient is not None and yaml_recipient != config_entry.options.get( + if yaml_recipient is not None and yaml_recipient != entry.options.get( f"{CONF_RECIPIENT}_from_yaml" ): new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient new_options[CONF_RECIPIENT] = yaml_recipient yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) - if ( - yaml_notify_name is not None - and yaml_notify_name != config_entry.options.get(f"{CONF_NAME}_from_yaml") + if yaml_notify_name is not None and yaml_notify_name != entry.options.get( + f"{CONF_NAME}_from_yaml" ): new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name new_options[CONF_NAME] = yaml_notify_name # Update entry if overrides were found if new_data or new_options: hass.config_entries.async_update_entry( - config_entry, - data={**config_entry.data, **new_data}, - options={**config_entry.options, **new_options}, + entry, + data={**entry.data, **new_data}, + options={**entry.options, **new_options}, ) # Get MAC address for use in unique ids. Being able to use something @@ -363,8 +362,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b Authorized one if username/pass specified (even if empty), unauthorized one otherwise. """ - username = config_entry.data.get(CONF_USERNAME) - password = config_entry.data.get(CONF_PASSWORD) + username = entry.data.get(CONF_USERNAME) + password = entry.data.get(CONF_PASSWORD) if username or password: connection: Connection = AuthorizedConnection( url, username=username, password=password, timeout=CONNECTION_TIMEOUT @@ -383,7 +382,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady from ex # Set up router and store reference to it - router = Router(config_entry, connection, url, mac, signal_update) + router = Router(entry, connection, url, mac, signal_update) hass.data[DOMAIN].routers[url] = router # Do initial data update @@ -409,7 +408,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_data["sw_version"] = sw_version device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=entry.entry_id, connections=router.device_connections, identifiers=router.device_identifiers, name=router.device_name, @@ -418,7 +417,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Forward config entry setup to platforms - hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_PLATFORMS) + hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_PLATFORMS) # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( @@ -427,8 +426,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b DOMAIN, { CONF_URL: url, - CONF_NAME: config_entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), - CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT), + CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), + CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT), }, hass.data[DOMAIN].hass_config, ) @@ -442,12 +441,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b router.update() # Set up periodic update - config_entry.async_on_unload( + entry.async_on_unload( async_track_time_interval(hass, _update_router, SCAN_INTERVAL) ) # Clean up at end - config_entry.async_on_unload( + entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) ) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index ddadb4feea5..5baa86d6926 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -136,11 +136,11 @@ def listen_for_instance_updates( ) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hyperion from a config entry.""" - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - token = config_entry.data.get(CONF_TOKEN) + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + token = entry.data.get(CONF_TOKEN) hyperion_client = await async_create_connect_hyperion_client( host, port, token=token, raw_connection=True @@ -190,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # We need 1 root client (to manage instances being removed/added) and then 1 client # per Hyperion server instance which is shared for all entities associated with # that instance. - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data[DOMAIN][entry.entry_id] = { CONF_ROOT_CLIENT: hyperion_client, CONF_INSTANCE_CLIENTS: {}, CONF_ON_UNLOAD: [], @@ -207,10 +207,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_registry = dr.async_get(hass) running_instances: set[int] = set() stopped_instances: set[int] = set() - existing_instances = hass.data[DOMAIN][config_entry.entry_id][ - CONF_INSTANCE_CLIENTS - ] - server_id = cast(str, config_entry.unique_id) + existing_instances = hass.data[DOMAIN][entry.entry_id][CONF_INSTANCE_CLIENTS] + server_id = cast(str, entry.unique_id) # In practice, an instance can be in 3 states as seen by this function: # @@ -239,7 +237,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME) async_dispatcher_send( hass, - SIGNAL_INSTANCE_ADD.format(config_entry.entry_id), + SIGNAL_INSTANCE_ADD.format(entry.entry_id), instance_num, instance_name, ) @@ -248,7 +246,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b for instance_num in set(existing_instances) - running_instances: del existing_instances[instance_num] async_dispatcher_send( - hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num + hass, SIGNAL_INSTANCE_REMOVE.format(entry.entry_id), instance_num ) # Ensure every device associated with this config entry is still in the list of @@ -258,7 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b for instance_num in running_instances | stopped_instances } for device_entry in dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id + device_registry, entry.entry_id ): for (kind, key) in device_entry.identifiers: if kind == DOMAIN and key in known_devices: @@ -275,15 +273,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def setup_then_listen() -> None: await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(config_entry, platform) + hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS ] ) assert hyperion_client if hyperion_client.instances is not None: await async_instances_to_clients_raw(hyperion_client.instances) - hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( - config_entry.add_update_listener(_async_entry_updated) + hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( + entry.add_update_listener(_async_entry_updated) ) hass.async_create_task(setup_then_listen()) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 473acac57cd..2af7434d2e4 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -29,22 +29,22 @@ PLATFORMS = [BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the component.""" hass.data.setdefault(DOMAIN, {}) - async_add_defaults(hass, config_entry) + async_add_defaults(hass, entry) - router = KeeneticRouter(hass, config_entry) + router = KeeneticRouter(hass, entry) await router.async_setup() - undo_listener = config_entry.add_update_listener(update_listener) + undo_listener = entry.add_update_listener(update_listener) - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data[DOMAIN][entry.entry_id] = { ROUTER: router, UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -97,14 +97,14 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -async def update_listener(hass, config_entry): +async def update_listener(hass, entry): """Handle options update.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) -def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry): +def async_add_defaults(hass: HomeAssistant, entry: ConfigEntry): """Populate default options.""" - host: str = config_entry.data[CONF_HOST] + host: str = entry.data[CONF_HOST] imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {}) options = { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, @@ -114,8 +114,8 @@ def async_add_defaults(hass: HomeAssistant, config_entry: ConfigEntry): CONF_INCLUDE_ARP: True, CONF_INCLUDE_ASSOCIATED: True, **imported_options, - **config_entry.options, + **entry.options, } - if options.keys() - config_entry.options.keys(): - hass.config_entries.async_update_entry(config_entry, options=options) + if options.keys() - entry.options.keys(): + hass.config_entries.async_update_entry(entry, options=options) diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index d52e0712a0b..12b9e51d2d6 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -30,15 +30,13 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up kraken from a config entry.""" - kraken_data = KrakenData(hass, config_entry) + kraken_data = KrakenData(hass, entry) await kraken_data.async_setup() hass.data[DOMAIN] = kraken_data - config_entry.async_on_unload( - config_entry.add_update_listener(async_options_updated) - ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_options_updated)) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 79e63e9b64d..20f72fd4410 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -5,7 +5,7 @@ from meteoclimatic import MeteoclimaticClient from meteoclimatic.exceptions import MeteoclimaticError from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL @@ -13,7 +13,7 @@ from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Meteoclimatic entry.""" station_code = entry.data[CONF_STATION_CODE] meteoclimatic_client = MeteoclimaticClient() @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return unload_ok diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 7059e935b2e..98507eae995 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -4,8 +4,8 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,7 +25,7 @@ def format_condition(condition): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Meteoclimatic weather platform.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 06fe466e8a4..a41f0018a4f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -25,24 +25,24 @@ PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" domain_data = hass.data.setdefault(DOMAIN, {}) # Create and store server instance. - unique_id = config_entry.unique_id + unique_id = entry.unique_id _LOGGER.debug( "Creating server instance for '%s' (%s)", - config_entry.data[CONF_NAME], - config_entry.data[CONF_HOST], + entry.data[CONF_NAME], + entry.data[CONF_HOST], ) - server = MinecraftServer(hass, unique_id, config_entry.data) + server = MinecraftServer(hass, unique_id, entry.data) domain_data[unique_id] = server await server.async_update() server.start_periodic_update() # Set up platforms. - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index a27e1a49ab1..5ba813ce368 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -13,17 +13,17 @@ from .onewirehub import CannotConnect, OneWireHub _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) onewirehub = OneWireHub(hass) try: - await onewirehub.initialize(config_entry) + await onewirehub.initialize(entry) except CannotConnect as exc: raise ConfigEntryNotReady() from exc - hass.data[DOMAIN][config_entry.entry_id] = onewirehub + hass.data[DOMAIN][entry.entry_id] = onewirehub async def cleanup_registry() -> None: # Get registries @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b registry_devices = [ entry.id for entry in dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id + device_registry, entry.entry_id ) ] # Remove devices that don't belong to any entity @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # wait until all required platforms are ready await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(config_entry, platform) + hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS ] ) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 49846a0ad0a..ed8f71bf634 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -30,14 +30,14 @@ from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up OpenWeatherMap as config entry.""" - name = config_entry.data[CONF_NAME] - api_key = config_entry.data[CONF_API_KEY] - latitude = config_entry.data.get(CONF_LATITUDE, hass.config.latitude) - longitude = config_entry.data.get(CONF_LONGITUDE, hass.config.longitude) - forecast_mode = _get_config_value(config_entry, CONF_MODE) - language = _get_config_value(config_entry, CONF_LANGUAGE) + name = entry.data[CONF_NAME] + api_key = entry.data[CONF_API_KEY] + latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) + longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) + forecast_mode = _get_config_value(entry, CONF_MODE) + language = _get_config_value(entry, CONF_LANGUAGE) config_dict = _get_owm_config(language) @@ -49,15 +49,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): await weather_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data[DOMAIN][entry.entry_id] = { ENTRY_NAME: name, ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - update_listener = config_entry.add_update_listener(async_update_options) - hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener + update_listener = entry.add_update_listener(async_update_options) + hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener return True @@ -84,20 +84,18 @@ async def async_migrate_entry(hass, entry): return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] + update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] update_listener() - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 2ab8f387bda..529fd601f30 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,7 +1,7 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -36,19 +36,19 @@ async def async_setup(hass: HomeAssistant, config: dict): for conf in config.get(DOMAIN, []): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT} + DOMAIN, data=conf, context={"source": SOURCE_IMPORT} ) ) return True -async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up pvpc hourly pricing from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index 785558ba34e..c7bf734b84c 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -35,9 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # unload srp client hass.data[SRP_ENERGY_DOMAIN] = None # Remove config entry - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 91edc7badeb..0a8bf7e05f8 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -19,9 +19,9 @@ from .const import ( ) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the StarLine device from a config entry.""" - account = StarlineAccount(hass, config_entry) + account = StarlineAccount(hass, entry) await account.update() await account.update_obd() if not account.api.available: @@ -29,27 +29,27 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - hass.data[DOMAIN][config_entry.entry_id] = account + hass.data[DOMAIN][entry.entry_id] = account device_registry = await hass.helpers.device_registry.async_get_registry() for device in account.api.devices.values(): device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, **account.device_info(device) + config_entry_id=entry.entry_id, **account.device_info(device) ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_set_scan_interval(call): """Set scan interval.""" - options = dict(config_entry.options) + options = dict(entry.options) options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] - hass.config_entries.async_update_entry(entry=config_entry, options=options) + hass.config_entries.async_update_entry(entry=entry, options=options) async def async_set_scan_obd_interval(call): """Set OBD info scan interval.""" - options = dict(config_entry.options) + options = dict(entry.options) options[CONF_SCAN_OBD_INTERVAL] = call.data[CONF_SCAN_INTERVAL] - hass.config_entries.async_update_entry(entry=config_entry, options=options) + hass.config_entries.async_update_entry(entry=entry, options=options) async def async_update(call=None): """Update all data.""" @@ -82,10 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ), ) - config_entry.async_on_unload( - config_entry.add_update_listener(async_options_updated) - ) - await async_options_updated(hass, config_entry) + entry.async_on_unload(entry.add_update_listener(async_options_updated)) + await async_options_updated(hass, entry) return True diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 69241f1cb44..fe00edd24b8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "Got %s lights: %s", len(lights), ", ".join(d.host for d in lights) ) - hass.async_create_task(forward_setup(config_entry, "light")) + hass.async_create_task(forward_setup(entry, "light")) if switches: _LOGGER.debug( @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ", ".join(d.host for d in switches), ) - hass.async_create_task(forward_setup(config_entry, "switch")) + hass.async_create_task(forward_setup(entry, "switch")) return True diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index b4d1f919238..661cb190877 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import ( SOURCE_TYPE_GPS, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EVENT, CONF_HOST, @@ -116,7 +117,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): """Configure a dispatcher connection based on a config entry.""" @callback diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 24c714dc437..d7b5230bf38 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -11,29 +11,29 @@ from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN PLATFORMS = ["light"] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up entries from config flow.""" # We setup the client here so if at some point we add any other entity for this device, # we will be able to properly share the connection. - uuid = config_entry.data[CONF_ENTRY_ID] - host = config_entry.data[CONF_ENTRY_HOST] + uuid = entry.data[CONF_ENTRY_ID] + host = entry.data[CONF_ENTRY_HOST] hass.data.setdefault(DOMAIN, {})[uuid] = twinkly_client.TwinklyClient( host, async_get_clientsession(hass) ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Remove a twinkly entry.""" # For now light entries don't have unload method, so we don't have to async_forward_entry_unload # However we still have to cleanup the shared client! - uuid = config_entry.data[CONF_ENTRY_ID] + uuid = entry.data[CONF_ENTRY_ID] hass.data[DOMAIN].pop(uuid) return True diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index a2bd2e6e88c..57267c92cf7 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -162,11 +162,11 @@ async def _async_signal_options_update( ) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the UpCloud config entry.""" manager = upcloud_api.CloudManager( - config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] ) try: @@ -182,20 +182,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Handle pre config entry (0.117) scan interval migration to options migrated_scan_interval = upcloud_data.scan_interval_migrations.pop( - config_entry.data[CONF_USERNAME], None + entry.data[CONF_USERNAME], None ) if migrated_scan_interval and ( - not config_entry.options.get(CONF_SCAN_INTERVAL) - or config_entry.options[CONF_SCAN_INTERVAL] - == DEFAULT_SCAN_INTERVAL.total_seconds() + not entry.options.get(CONF_SCAN_INTERVAL) + or entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL.total_seconds() ): update_interval = migrated_scan_interval hass.config_entries.async_update_entry( - config_entry, + entry, options={CONF_SCAN_INTERVAL: update_interval.total_seconds()}, ) - elif config_entry.options.get(CONF_SCAN_INTERVAL): - update_interval = timedelta(seconds=config_entry.options[CONF_SCAN_INTERVAL]) + elif entry.options.get(CONF_SCAN_INTERVAL): + update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL]) else: update_interval = DEFAULT_SCAN_INTERVAL @@ -203,28 +202,26 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass, update_interval=update_interval, cloud_manager=manager, - username=config_entry.data[CONF_USERNAME], + username=entry.data[CONF_USERNAME], ) # Call the UpCloud API to refresh data await coordinator.async_config_entry_first_refresh() # Listen to config entry updates - config_entry.async_on_unload( - config_entry.add_update_listener(_async_signal_options_update) - ) - config_entry.async_on_unload( + entry.async_on_unload(entry.add_update_listener(_async_signal_options_update)) + entry.async_on_unload( async_dispatcher_connect( hass, - _config_entry_update_signal_name(config_entry), + _config_entry_update_signal_name(entry), coordinator.async_update_config, ) ) - upcloud_data.coordinators[config_entry.data[CONF_USERNAME]] = coordinator + upcloud_data.coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_DOMAINS) + hass.config_entries.async_setup_platforms(entry, CONFIG_ENTRY_DOMAINS) return True diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 7edf7b99d36..24be3b119dc 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -92,13 +92,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id) + _LOGGER.debug("Setting up config entry: %s", entry.unique_id) # Discover and construct. - udn = config_entry.data[CONFIG_ENTRY_UDN] - st = config_entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name + udn = entry.data[CONFIG_ENTRY_UDN] + st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name try: device = await async_construct_device(hass, udn, st) except asyncio.TimeoutError as err: @@ -112,31 +112,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device # Ensure entry has a unique_id. - if not config_entry.unique_id: + if not entry.unique_id: _LOGGER.debug( "Setting unique_id: %s, for config_entry: %s", device.unique_id, - config_entry, + entry, ) hass.config_entries.async_update_entry( - entry=config_entry, + entry=entry, unique_id=device.unique_id, ) # Ensure entry has a hostname, for older entries. if ( - CONFIG_ENTRY_HOSTNAME not in config_entry.data - or config_entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname + CONFIG_ENTRY_HOSTNAME not in entry.data + or entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname ): hass.config_entries.async_update_entry( - entry=config_entry, - data={CONFIG_ENTRY_HOSTNAME: device.hostname, **config_entry.data}, + entry=entry, + data={CONFIG_ENTRY_HOSTNAME: device.hostname, **entry.data}, ) # Create device registry entry. device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, + config_entry_id=entry.entry_id, connections={(dr.CONNECTION_UPNP, device.udn)}, identifiers={(DOMAIN, device.udn)}, name=device.name, @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Create sensors. _LOGGER.debug("Enabling sensors") - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Start device updater. await device.async_start() diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 096c6a8aa15..feac63f694b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -83,30 +83,30 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Do setup of vera.""" # Use options entered during initial config flow or provided from configuration.yml - if config_entry.data.get(CONF_LIGHTS) or config_entry.data.get(CONF_EXCLUDE): + if entry.data.get(CONF_LIGHTS) or entry.data.get(CONF_EXCLUDE): hass.config_entries.async_update_entry( - entry=config_entry, - data=config_entry.data, + entry=entry, + data=entry.data, options=new_options( - config_entry.data.get(CONF_LIGHTS, []), - config_entry.data.get(CONF_EXCLUDE, []), + entry.data.get(CONF_LIGHTS, []), + entry.data.get(CONF_EXCLUDE, []), ), ) - saved_light_ids = config_entry.options.get(CONF_LIGHTS, []) - saved_exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + saved_light_ids = entry.options.get(CONF_LIGHTS, []) + saved_exclude_ids = entry.options.get(CONF_EXCLUDE, []) - base_url = config_entry.data[CONF_CONTROLLER] + base_url = entry.data[CONF_CONTROLLER] light_ids = fix_device_id_list(saved_light_ids) exclude_ids = fix_device_id_list(saved_exclude_ids) # If the ids were corrected. Update the config entry. if light_ids != saved_light_ids or exclude_ids != saved_exclude_ids: hass.config_entries.async_update_entry( - entry=config_entry, options=new_options(light_ids, exclude_ids) + entry=entry, options=new_options(light_ids, exclude_ids) ) # Initialize the Vera controller. @@ -139,15 +139,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b controller=controller, devices=vera_devices, scenes=vera_scenes, - config_entry=config_entry, + config_entry=entry, ) - set_controller_data(hass, config_entry, controller_data) + set_controller_data(hass, entry, controller_data) # Forward the config data to the necessary platforms. for platform in get_configured_platforms(controller_data): hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def stop_subscription(event): @@ -155,13 +155,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b controller.stop() await hass.async_add_executor_job(controller.start) - config_entry.async_on_unload( + entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) ) - config_entry.async_on_unload( - config_entry.add_update_listener(_async_update_listener) - ) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 7c1ed7e8fa7..aec6f38a1b1 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -56,19 +56,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load the saved entities.""" hass.data.setdefault(DOMAIN, {}) if ( CONF_APPS not in hass.data[DOMAIN] - and config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV ): coordinator = VizioAppsDataUpdateCoordinator(hass) await coordinator.async_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 57382689f61..1b9db0e947a 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -12,18 +12,16 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load the saved entities.""" - if config_entry.unique_id is not None: - hass.config_entries.async_update_entry(config_entry, unique_id=None) + if entry.unique_id is not None: + hass.config_entries.async_update_entry(entry, unique_id=None) ent_reg = async_get(hass) - for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id): - ent_reg.async_update_entity( - entity.entity_id, new_unique_id=config_entry.entry_id - ) + for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): + ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index f36e4b0df32..60ca4f9df53 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -32,17 +32,17 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up wiffi from a config entry, config_entry contains data from config entry database.""" - if not config_entry.update_listeners: - config_entry.add_update_listener(async_update_options) + if not entry.update_listeners: + entry.add_update_listener(async_update_options) # create api object api = WiffiIntegrationApi(hass) - api.async_setup(config_entry) + api.async_setup(entry) # store api object - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = api + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api try: await api.server.start_server() @@ -50,29 +50,27 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): if exc.errno != errno.EADDRINUSE: _LOGGER.error("Start_server failed, errno: %d", exc.errno) return False - _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT]) + _LOGGER.error("Port %s already in use", entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id] + api: WiffiIntegrationApi = hass.data[DOMAIN][entry.entry_id] await api.server.close_server() - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - api = hass.data[DOMAIN].pop(config_entry.entry_id) + api = hass.data[DOMAIN].pop(entry.entry_id) api.shutdown() return unload_ok diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 32a3126de1e..4965e9705d1 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from functools import partial from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, @@ -16,16 +17,18 @@ from .const import DOMAIN PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"] -async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): """Set up Xbox Live friends.""" - coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ + coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ "coordinator" ] update_friends = partial(async_update_friends, coordinator, {}, async_add_entities) unsub = coordinator.async_add_listener(update_friends) - hass.data[DOMAIN][config_entry.entry_id]["binary_sensor_unsub"] = unsub + hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"] = unsub update_friends() diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 892e77dc7f6..d03974d1dfe 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -8,7 +8,7 @@ from voluptuous.schema_builder import raises from homeassistant.components import wallbox from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -31,7 +31,7 @@ test_response_rounding_error = json.loads( ) -async def test_wallbox_setup_entry(hass: HomeAssistantType): +async def test_wallbox_setup_entry(hass: HomeAssistant): """Test Wallbox Setup.""" with requests_mock.Mocker() as m: m.get( @@ -55,7 +55,7 @@ async def test_wallbox_setup_entry(hass: HomeAssistantType): assert await wallbox.async_setup_entry(hass, entry) is False -async def test_wallbox_unload_entry(hass: HomeAssistantType): +async def test_wallbox_unload_entry(hass: HomeAssistant): """Test Wallbox Unload.""" hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} @@ -67,7 +67,7 @@ async def test_wallbox_unload_entry(hass: HomeAssistantType): await wallbox.async_unload_entry(hass, entry) -async def test_get_data(hass: HomeAssistantType): +async def test_get_data(hass: HomeAssistant): """Test hub class, get_data.""" station = ("12345",) @@ -90,7 +90,7 @@ async def test_get_data(hass: HomeAssistantType): assert await hub.async_get_data() -async def test_get_data_rounding_error(hass: HomeAssistantType): +async def test_get_data_rounding_error(hass: HomeAssistant): """Test hub class, get_data with rounding error.""" station = ("12345",) @@ -113,7 +113,7 @@ async def test_get_data_rounding_error(hass: HomeAssistantType): assert await hub.async_get_data() -async def test_authentication_exception(hass: HomeAssistantType): +async def test_authentication_exception(hass: HomeAssistant): """Test hub class, authentication raises exception.""" station = ("12345",) @@ -142,7 +142,7 @@ async def test_authentication_exception(hass: HomeAssistantType): assert await hub.async_get_data() -async def test_get_data_exception(hass: HomeAssistantType): +async def test_get_data_exception(hass: HomeAssistant): """Test hub class, authentication raises exception.""" station = ("12345",) From b6355bcb485702f5a814e3a3e4eb2e1c8f6a75d0 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 27 May 2021 18:37:54 +0300 Subject: [PATCH 018/750] Add myself to Switcher codeowners (#51158) --- CODEOWNERS | 2 +- homeassistant/components/switcher_kis/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index faa623456f1..7f2dfc1454e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -481,7 +481,7 @@ homeassistant/components/surepetcare/* @benleb homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen -homeassistant/components/switcher_kis/* @tomerfi +homeassistant/components/switcher_kis/* @tomerfi @thecode homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthing/* @zhulik homeassistant/components/syncthru/* @nielstron diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 7344e2d05c0..a5af1187f07 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -2,7 +2,7 @@ "domain": "switcher_kis", "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", - "codeowners": ["@tomerfi"], + "codeowners": ["@tomerfi","@thecode"], "requirements": ["aioswitcher==1.2.1"], "iot_class": "local_push" } From d1c4d0de49ef82e284c8c9817e1fe7494c2127d6 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 27 May 2021 11:39:06 -0400 Subject: [PATCH 019/750] Use bool annotations for setup entries (#51166) --- homeassistant/components/aemet/__init__.py | 2 +- homeassistant/components/airnow/__init__.py | 2 +- homeassistant/components/almond/__init__.py | 2 +- homeassistant/components/arcam_fmj/__init__.py | 2 +- homeassistant/components/asuswrt/__init__.py | 2 +- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/aurora/__init__.py | 2 +- homeassistant/components/blebox/__init__.py | 2 +- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- homeassistant/components/cert_expiry/__init__.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/dexcom/__init__.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/dsmr/__init__.py | 2 +- homeassistant/components/elkm1/__init__.py | 2 +- homeassistant/components/emonitor/__init__.py | 2 +- homeassistant/components/epson/__init__.py | 2 +- homeassistant/components/faa_delays/__init__.py | 2 +- homeassistant/components/flick_electric/__init__.py | 2 +- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flume/__init__.py | 2 +- homeassistant/components/foscam/__init__.py | 2 +- homeassistant/components/freebox/__init__.py | 2 +- homeassistant/components/garmin_connect/__init__.py | 2 +- homeassistant/components/google_travel_time/__init__.py | 2 +- homeassistant/components/gree/__init__.py | 2 +- homeassistant/components/harmony/__init__.py | 2 +- homeassistant/components/heos/__init__.py | 2 +- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/huisbaasje/__init__.py | 2 +- .../components/hunterdouglas_powerview/__init__.py | 2 +- homeassistant/components/hvv_departures/__init__.py | 2 +- homeassistant/components/ialarm/__init__.py | 2 +- homeassistant/components/iaqualink/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 2 +- homeassistant/components/kmtronic/__init__.py | 2 +- homeassistant/components/kodi/__init__.py | 2 +- homeassistant/components/konnected/__init__.py | 2 +- homeassistant/components/kulersky/__init__.py | 2 +- homeassistant/components/litterrobot/__init__.py | 2 +- homeassistant/components/mazda/__init__.py | 2 +- homeassistant/components/melcloud/__init__.py | 2 +- homeassistant/components/metoffice/__init__.py | 2 +- homeassistant/components/monoprice/__init__.py | 2 +- homeassistant/components/mullvad/__init__.py | 2 +- homeassistant/components/myq/__init__.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/netatmo/__init__.py | 2 +- homeassistant/components/nexia/__init__.py | 2 +- homeassistant/components/nightscout/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 2 +- homeassistant/components/nut/__init__.py | 2 +- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/omnilogic/__init__.py | 2 +- homeassistant/components/onvif/__init__.py | 2 +- homeassistant/components/openweathermap/__init__.py | 2 +- homeassistant/components/ozw/__init__.py | 6 ++++-- homeassistant/components/philips_js/__init__.py | 2 +- homeassistant/components/picnic/__init__.py | 2 +- homeassistant/components/plaato/__init__.py | 2 +- homeassistant/components/plum_lightpad/__init__.py | 2 +- homeassistant/components/point/__init__.py | 2 +- homeassistant/components/poolsense/__init__.py | 2 +- homeassistant/components/powerwall/__init__.py | 2 +- homeassistant/components/profiler/__init__.py | 4 ++-- homeassistant/components/progettihwsw/__init__.py | 2 +- homeassistant/components/pvpc_hourly_pricing/__init__.py | 2 +- homeassistant/components/rachio/__init__.py | 2 +- homeassistant/components/risco/__init__.py | 2 +- homeassistant/components/rituals_perfume_genie/__init__.py | 2 +- homeassistant/components/rpi_power/__init__.py | 2 +- homeassistant/components/screenlogic/__init__.py | 2 +- homeassistant/components/sense/__init__.py | 2 +- homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/smappee/__init__.py | 2 +- homeassistant/components/smart_meter_texas/__init__.py | 2 +- homeassistant/components/smarthab/__init__.py | 2 +- homeassistant/components/smartthings/__init__.py | 4 ++-- homeassistant/components/sms/__init__.py | 2 +- homeassistant/components/solarlog/__init__.py | 2 +- homeassistant/components/soma/__init__.py | 2 +- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/somfy_mylink/__init__.py | 2 +- homeassistant/components/squeezebox/__init__.py | 2 +- homeassistant/components/srp_energy/__init__.py | 2 +- homeassistant/components/system_bridge/__init__.py | 2 +- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/totalconnect/__init__.py | 2 +- homeassistant/components/tradfri/__init__.py | 2 +- homeassistant/components/tuya/__init__.py | 2 +- homeassistant/components/twinkly/__init__.py | 4 ++-- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/vilfo/__init__.py | 2 +- homeassistant/components/volumio/__init__.py | 2 +- homeassistant/components/wallbox/__init__.py | 2 +- homeassistant/components/wemo/__init__.py | 2 +- homeassistant/components/wiffi/__init__.py | 2 +- homeassistant/components/wilight/__init__.py | 2 +- homeassistant/components/wolflink/__init__.py | 2 +- homeassistant/components/xbox/__init__.py | 2 +- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/zerproc/__init__.py | 2 +- 103 files changed, 109 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index e958195b215..7a20e77f0b0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -19,7 +19,7 @@ from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 0b27a4a9dfd..52ee1a0e8fc 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -36,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirNow from a config entry.""" api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index b8e13cc1dee..5d3b5a86942 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -101,7 +101,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 3b49008cb5f..905e31c798b 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" entries = hass.data[DOMAIN_DATA_ENTRIES] tasks = hass.data[DOMAIN_DATA_TASKS] diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index ad3cea1106b..af7f3b05e33 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -111,7 +111,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AsusWrt platform.""" # import options from yaml if empty diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index e6347563bc2..af5eff67f57 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -24,7 +24,7 @@ DOMAIN = "atag" PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Atag integration from a config entry.""" async def _async_update_data(): diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index e8dc98a18b7..faccefda500 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aurora from a config entry.""" conf = entry.data diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index fe2265ed78d..33d09f460db 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -21,7 +21,7 @@ PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"] PARALLEL_UPDATES = 0 -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BleBox devices from a config entry.""" websession = async_get_clientsession(hass) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 79461082acd..038a8696818 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -101,7 +101,7 @@ def _async_migrate_options_from_data_if_missing(hass, entry): hass.config_entries.async_update_entry(entry, data=data, options=options) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BMW Connected Drive from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(DATA_ENTRIES, {}) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index f91eaab49b6..c4381b65c49 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(hours=12) PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load the saved entities.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 78e27d86f8e..6e4af61e24b 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["light"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Control4 from a config entry.""" hass.data.setdefault(DOMAIN, {}) entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {}) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 63b0c7de25e..504f8cd9f86 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -27,7 +27,7 @@ PLATFORMS = ["climate", "sensor", "switch"] CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with Daikin.""" conf = entry.data # For backwards compat, set unique ID diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 1c02a86ca42..68622a23350 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=180) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Dexcom from a config entry.""" try: dexcom = await hass.async_add_executor_job( diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 30c2613f0d5..f0579ef900b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -90,7 +90,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DoorBird from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index ba876111a4e..61c7660fc69 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DSMR from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index ff2f2533d24..6a96a73de22 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -195,7 +195,7 @@ def _async_find_matching_config_entry(hass, prefix): return entry -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elk-M1 Control from a config entry.""" conf = entry.data diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 516f38d64c2..69c8b907b72 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -19,7 +19,7 @@ DEFAULT_UPDATE_RATE = 60 PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SiteSage Emonitor from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 1982731b9ef..e60df7dc8bc 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -41,7 +41,7 @@ async def validate_projector( return epson_proj -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up epson from a config entry.""" projector = await validate_projector( hass=hass, diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 56cf9ad13bc..c270a878d49 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up FAA Delays from a config entry.""" code = entry.data[CONF_ID] diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index ff9b737cd00..690cbe03cdd 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -24,7 +24,7 @@ CONF_ID_TOKEN = "id_token" PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flick Electric from a config entry.""" auth = HassFlickAuth(hass, entry) diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 890f18ee3b7..734c4d9e766 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up flo from a config entry.""" session = async_get_clientsession(hass) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 9bdc918be9c..4fc66d0ee70 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -53,7 +53,7 @@ def _setup_entry(hass: HomeAssistant, entry: ConfigEntry): return flume_auth, flume_devices, http_session -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up flume from a config entry.""" flume_auth, flume_devices, http_session = await hass.async_add_executor_job( diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 148768d6289..9d825ed0851 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -13,7 +13,7 @@ from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRES PLATFORMS = ["camera"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up foscam from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 44816a5c8ae..bb308e154ef 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -39,7 +39,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Freebox entry.""" router = FreeboxRouter(hass, entry) await router.setup() diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 4ac157707fc..af08a86abb2 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -23,7 +23,7 @@ PLATFORMS = ["sensor"] MIN_SCAN_INTERVAL = timedelta(minutes=10) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garmin Connect from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 231a2db22e2..88fe587fbd0 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -12,7 +12,7 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Maps Travel Time from a config entry.""" if entry.unique_id is not None: hass.config_entries.async_update_entry(entry, unique_id=None) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index b873d5ba4d3..b91324ba4b3 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [CLIMATE_DOMAIN, SWITCH_DOMAIN] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Gree Climate from a config entry.""" hass.data.setdefault(DOMAIN, {}) gree_discovery = DiscoveryService(hass) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index d0172bf7378..e76e5559f9d 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -23,7 +23,7 @@ from .data import HarmonyData _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Logitech Harmony Hub from a config entry.""" # As there currently is no way to import options from yaml # when setting up a config entry, we fallback to adding diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 3a9bedbb376..7490c1e5be1 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -67,7 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Initialize config entry which represents the HEOS controller.""" # For backwards compat if entry.unique_id is None: diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 77a9ae27400..32b528f567c 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -231,7 +231,7 @@ def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): return False -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HomeKit from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index f89c9f07625..8bd07474705 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -28,7 +28,7 @@ PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client huisbaasje = Huisbaasje( diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index d9a52446028..7b945d9bdfe 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -59,7 +59,7 @@ PLATFORMS = ["cover", "scene", "sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hunter Douglas PowerView from a config entry.""" config = entry.data diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index acdb3dcfb64..d57fe20f95e 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -13,7 +13,7 @@ from .hub import GTIHub PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HVV from a config entry.""" hub = GTIHub( diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index a74eea7ba07..1fb35e23c9e 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -18,7 +18,7 @@ PLATFORMS = ["alarm_control_panel"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up iAlarm config.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 4ed9efd06f2..2bd0cade3b9 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -85,7 +85,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aqualink from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 28789849944..38089a6e17f 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up JuiceNet from a config entry.""" config = entry.data diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 7dd5b087a87..5226dfb4f26 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -20,7 +20,7 @@ PLATFORMS = ["switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up kmtronic from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) auth = Auth( diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index fe318b103d1..0b2b1b8047b 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["media_player"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Kodi from a config entry.""" conn = get_kodi_connection( entry.data[CONF_HOST], diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 857521b9fad..4b5890532d1 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -250,7 +250,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up panel from a config entry.""" client = AlarmPanel(hass, entry) # creates a panel data store in hass.data[DOMAIN][CONF_DEVICES] diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 6409d435bf3..03819c360d6 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -8,7 +8,7 @@ from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN PLATFORMS = ["light"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Kuler Sky from a config entry.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 424a6a92aba..25500a1fcfb 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -12,7 +12,7 @@ from .hub import LitterRobotHub PLATFORMS = ["sensor", "switch", "vacuum"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 2c480aaf606..469e28eb829 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -43,7 +43,7 @@ async def with_timeout(task, timeout_seconds=10): return await task -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Mazda Connected Services from a config entry.""" email = entry.data[CONF_EMAIL] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 7b42c1f42e8..12b80554933 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -61,7 +61,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigEntry): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with MELClooud.""" conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 9bf9e44b72a..f73c87f94ec 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "weather"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Met Office entry.""" latitude = entry.data[CONF_LATITUDE] diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index f543220b5b9..9ee6128c784 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -22,7 +22,7 @@ PLATFORMS = ["media_player"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Monoprice 6-Zone Amplifier from a config entry.""" port = entry.data[CONF_PORT] diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index d89c947a4f3..44d10a66d5d 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN PLATFORMS = ["binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: dict): +async def async_setup_entry(hass: HomeAssistant, entry: dict) -> bool: """Set up Mullvad VPN integration.""" async def async_get_mullvad_api_data(): diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index a299968712a..063f044117e 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -17,7 +17,7 @@ from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTER _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up MyQ from a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 5cd84effbc8..fb488763750 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -135,7 +135,7 @@ class SignalUpdateCallback: self._hass.bus.async_fire(NEST_EVENT, message) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 354ce2cf942..c401b46f981 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -96,7 +96,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Netatmo from a config entry.""" implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 65be22b57f1..e9f31749042 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) DEFAULT_UPDATE_RATE = 120 -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure the base Nexia device for Home Assistant.""" conf = entry.data diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 8608386c483..69d79d1cecb 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -18,7 +18,7 @@ PLATFORMS = ["sensor"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nightscout from a config entry.""" server_url = entry.data[CONF_URL] api_key = entry.data.get(CONF_API_KEY) diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index db50a9a70d9..08cacd2bf76 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -30,7 +30,7 @@ def _get_thermostat(api, serial_number): return api.get_thermostat(serial_number) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NuHeat from a config entry.""" conf = entry.data diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 77458b2cfb7..5b5389f0270 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -36,7 +36,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" config = entry.data diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 47465739250..0e00c848970 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -93,7 +93,7 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator): ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a National Weather Service entry.""" latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 8d2071dee7c..556c100033b 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "switch"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Omnilogic from a config entry.""" conf = entry.data diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index f90ccb16760..5c44cdf1750 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -59,7 +59,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up ONVIF from a config entry.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index ed8f71bf634..49bb870271e 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -30,7 +30,7 @@ from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenWeatherMap as config entry.""" name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 7890129dd91..6d9b474977d 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -56,7 +56,9 @@ DATA_DEVICES = "zwave-mqtt-devices" DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up ozw from a config entry.""" hass.data.setdefault(DOMAIN, {}) ozw_data = hass.data[DOMAIN][entry.entry_id] = {} @@ -298,7 +300,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # cleanup platforms unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index f21337f512e..ffa2109a6e5 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -27,7 +27,7 @@ PLATFORMS = ["media_player", "remote"] LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Philips TV from a config entry.""" tvapi = PhilipsTV( diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index 055faadb784..1a2164dafed 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -20,7 +20,7 @@ def create_picnic_client(entry: ConfigEntry): ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Picnic from a config entry.""" picnic_client = await hass.async_add_executor_job(create_picnic_client, entry) picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry) diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index d73b997398a..c214061c416 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -83,7 +83,7 @@ WEBHOOK_SCHEMA = vol.Schema( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure based on config entry.""" hass.data.setdefault(DOMAIN, {}) use_webhook = entry.data[CONF_USE_WEBHOOK] diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index ab370f53731..9f69c8579a4 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plum Lightpad from a config entry.""" _LOGGER.debug("Setting up config entry with ID = %s", entry.unique_id) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 45f58949e77..43f489d5132 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -74,7 +74,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Point from a config entry.""" async def token_saver(token, **kwargs): diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 89e340ee95e..5ec1cb475b5 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -24,7 +24,7 @@ PLATFORMS = ["sensor", "binary_sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up PoolSense from a config entry.""" poolsense = PoolSense( diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 0f63bf97986..3bc4dc9b035 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -83,7 +83,7 @@ async def _async_handle_api_changed_error( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tesla Powerwall from a config entry.""" entry_id = entry.entry_id diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index e6bc68ba918..e2d44451ec2 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -51,7 +51,7 @@ LOG_INTERVAL_SUB = "log_interval_subscription" _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Profiler from a config entry.""" lock = asyncio.Lock() domain_data = hass.data[DOMAIN] = {} @@ -194,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" for service in SERVICES: hass.services.async_remove(domain=DOMAIN, service=service) diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 78ea16bb26c..55ed9c0241b 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -12,7 +12,7 @@ from .const import DOMAIN PLATFORMS = ["switch", "binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up ProgettiHWSW Automation from a config entry.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI( diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 529fd601f30..22ad590659e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -43,7 +43,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pvpc hourly pricing from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 3f75537cc8d..3e8e26e2a13 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -39,7 +39,7 @@ async def async_remove_entry(hass, entry): await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID]) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Rachio config entry.""" config = entry.data diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 48c50f9cc46..4b33873e88d 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -27,7 +27,7 @@ LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Risco from a config entry.""" data = entry.data risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 65c1a2dd97c..0ec0ca47a09 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL = timedelta(seconds=30) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Rituals Perfume Genie from a config entry.""" session = async_get_clientsession(hass) account = Account(EMPTY_CREDENTIALS, EMPTY_CREDENTIALS, session) diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 305ad7d1f62..eeb7c4fe181 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant PLATFORMS = ["binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Raspberry Pi Power Supply Checker from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 2225ef3d9dd..521a1ea798c 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" mac = entry.unique_id # Attempt to re-discover named gateway to follow IP changes diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index e431fe2487b..bfc3f42b421 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -55,7 +55,7 @@ class SenseDevicesData: return self._data_by_device.get(sense_device_id) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sense from a config entry.""" entry_data = entry.data diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index d2c56217afe..ab0facea920 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -66,7 +66,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly from a config entry.""" hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 3386f7340eb..1037d399e64 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Smappee from a zeroconf or config entry.""" if CONF_IP_ADDRESS in entry.data: if helper.is_smappee_genius(entry.data[CONF_SERIALNUMBER]): diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 82a8ccb354b..3e88221851b 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Smart Meter Texas from a config entry.""" username = entry.data[CONF_USERNAME] diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 3777f35dbc2..ec4d2c9cad6 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -52,7 +52,7 @@ async def async_setup(hass, config) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry for SmartHab integration.""" # Assign configuration variables diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 00ea0eb681e..231cfa95263 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -62,7 +62,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle migration of a previous version config entry. A config entry created under a previous version must go through the @@ -84,7 +84,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry): return False -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Initialize config entry which represents an installed SmartApp.""" # For backwards compat if entry.unique_id is None: diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 55238c5cf39..52ea32c96d1 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -36,7 +36,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure Gammu state machine.""" device = entry.data[CONF_DEVICE] diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index f48dcfc6267..b3cfebe9abc 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for solarlog.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index e90fba62824..10ebd26bde2 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -45,7 +45,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Soma from a config entry.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index f159b19c92a..cc7499c3492 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -69,7 +69,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Somfy from a config entry.""" # Backwards compat if "auth_implementation" not in entry.data: diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index ae6a77a1e2c..5377846a4c1 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Somfy MyLink from a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index f680c4f5f2f..9bdc8ac9669 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [MP_DOMAIN] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Logitech Squeezebox from a config entry.""" hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index c7bf734b84c..0e25e3f21f6 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the SRP Energy component from a config entry.""" # Store an SrpEnergyClient object for your srp_energy to access try: diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index cb78603f6dc..10ee4165295 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -59,7 +59,7 @@ SERVICE_OPEN_SCHEMA = vol.Schema( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up System Bridge from a config entry.""" client = Bridge( diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 37ee3b47b9b..db42df62154 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -38,7 +38,7 @@ SCAN_INTERVAL = timedelta(minutes=5) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tado from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 7026abc34f9..2183448eed7 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -14,7 +14,7 @@ PLATFORMS = ["alarm_control_panel", "binary_sensor"] CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up upon config entry in user interface.""" conf = entry.data username = conf[CONF_USERNAME] diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bf8fa00bbc8..cf39d3d6c05 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -100,7 +100,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Create a gateway.""" # host, identity, key, allow_tradfri_groups tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 86ba0e12c61..595350324f9 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -93,7 +93,7 @@ def _update_query_interval(hass, interval): _LOGGER.warning(ex) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tuya platform.""" tuya = TuyaApi() diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index d7b5230bf38..3a9a2a8faa2 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -11,7 +11,7 @@ from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN PLATFORMS = ["light"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up entries from config flow.""" # We setup the client here so if at some point we add any other entity for this device, @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove a twinkly entry.""" # For now light entries don't have unload method, so we don't have to async_forward_entry_unload diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 47f51d8b26e..b798023c465 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -43,7 +43,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Establish connection with velbus.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index 59387fa81c8..9a65ba3c400 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -20,7 +20,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Vilfo Router from a config entry.""" host = entry.data[CONF_HOST] access_token = entry.data[CONF_ACCESS_TOKEN] diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index f9b9432d755..6e0db0f73eb 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -13,7 +13,7 @@ from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN PLATFORMS = ["media_player"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Volumio from a config entry.""" volumio = Volumio( diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 97b2ea12f35..275ccddde33 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -89,7 +89,7 @@ class WallboxHub: return self._coordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Wallbox from a config entry.""" wallbox = WallboxHub( entry.data[CONF_STATION], diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index cb3beb9b67a..dfdd0a0adb6 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -98,7 +98,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a wemo config entry.""" config = hass.data[DOMAIN].pop("config") diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 60ca4f9df53..55b13921c1c 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "binary_sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up wiffi from a config entry, config_entry contains data from config entry database.""" if not entry.update_listeners: entry.add_update_listener(async_update_options) diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 0ac2713994b..3c3c24db793 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -13,7 +13,7 @@ DOMAIN = "wilight" PLATFORMS = ["cover", "fan", "light"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a wilight config entry.""" parent = WiLightParent(hass, entry) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 06f3408c6a5..7286f28568d 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Wolf SmartSet Service from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index db278d0da43..6e651cdbcf3 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up xbox from a config entry.""" implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index b18f18b6fa4..8ed2164f75c 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -271,7 +271,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES] entry_data = data_config_entries[entry.entry_id] diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index 8d42c81162f..8643066f59c 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -17,7 +17,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Zerproc from a config entry.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} From 7dff4d6ad749f8eccbf7e1de27878c835aa5ed43 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 May 2021 17:39:43 +0200 Subject: [PATCH 020/750] Define climate entity attributes as class variables (#51006) --- homeassistant/components/climate/__init__.py | 86 +++++++++++++------- homeassistant/components/toon/climate.py | 66 ++++++--------- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0369933dd0a..cd559a2e345 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,7 +1,6 @@ """Provides functionality to interact with climate devices.""" from __future__ import annotations -from abc import abstractmethod from datetime import timedelta import functools as ft import logging @@ -171,6 +170,31 @@ async def async_unload_entry(hass: HomeAssistant, entry): class ClimateEntity(Entity): """Base class for climate entities.""" + _attr_current_humidity: int | None = None + _attr_current_temperature: float | None = None + _attr_fan_mode: str | None + _attr_fan_modes: list[str] | None + _attr_hvac_action: str | None = None + _attr_hvac_mode: str + _attr_hvac_modes: list[str] + _attr_is_aux_heat: bool | None + _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY + _attr_max_temp: float + _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY + _attr_min_temp: float + _attr_precision: float + _attr_preset_mode: str | None + _attr_preset_modes: list[str] | None + _attr_supported_features: int + _attr_swing_mode: str | None + _attr_swing_modes: list[str] | None + _attr_target_humidity: int | None = None + _attr_target_temperature_high: float | None + _attr_target_temperature_low: float | None + _attr_target_temperature_step: float | None = None + _attr_target_temperature: float | None = None + _attr_temperature_unit: str + @property def state(self) -> str: """Return the current state.""" @@ -179,6 +203,8 @@ class ClimateEntity(Entity): @property def precision(self) -> float: """Return the precision of the system.""" + if hasattr(self, "_attr_precision"): + return self._attr_precision if self.hass.config.units.temperature_unit == TEMP_CELSIUS: return PRECISION_TENTHS return PRECISION_WHOLE @@ -277,33 +303,33 @@ class ClimateEntity(Entity): @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" - raise NotImplementedError() + return self._attr_temperature_unit @property def current_humidity(self) -> int | None: """Return the current humidity.""" - return None + return self._attr_current_humidity @property def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" - return None + return self._attr_target_humidity @property - @abstractmethod def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ + return self._attr_hvac_mode @property - @abstractmethod def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ + return self._attr_hvac_modes @property def hvac_action(self) -> str | None: @@ -311,22 +337,22 @@ class ClimateEntity(Entity): Need to be one of CURRENT_HVAC_*. """ - return None + return self._attr_hvac_action @property def current_temperature(self) -> float | None: """Return the current temperature.""" - return None + return self._attr_current_temperature @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return None + return self._attr_target_temperature @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - return None + return self._attr_target_temperature_step @property def target_temperature_high(self) -> float | None: @@ -334,7 +360,7 @@ class ClimateEntity(Entity): Requires SUPPORT_TARGET_TEMPERATURE_RANGE. """ - raise NotImplementedError + return self._attr_target_temperature_high @property def target_temperature_low(self) -> float | None: @@ -342,7 +368,7 @@ class ClimateEntity(Entity): Requires SUPPORT_TARGET_TEMPERATURE_RANGE. """ - raise NotImplementedError + return self._attr_target_temperature_low @property def preset_mode(self) -> str | None: @@ -350,7 +376,7 @@ class ClimateEntity(Entity): Requires SUPPORT_PRESET_MODE. """ - raise NotImplementedError + return self._attr_preset_mode @property def preset_modes(self) -> list[str] | None: @@ -358,7 +384,7 @@ class ClimateEntity(Entity): Requires SUPPORT_PRESET_MODE. """ - raise NotImplementedError + return self._attr_preset_modes @property def is_aux_heat(self) -> bool | None: @@ -366,7 +392,7 @@ class ClimateEntity(Entity): Requires SUPPORT_AUX_HEAT. """ - raise NotImplementedError + return self._attr_is_aux_heat @property def fan_mode(self) -> str | None: @@ -374,7 +400,7 @@ class ClimateEntity(Entity): Requires SUPPORT_FAN_MODE. """ - raise NotImplementedError + return self._attr_fan_mode @property def fan_modes(self) -> list[str] | None: @@ -382,7 +408,7 @@ class ClimateEntity(Entity): Requires SUPPORT_FAN_MODE. """ - raise NotImplementedError + return self._attr_fan_modes @property def swing_mode(self) -> str | None: @@ -390,7 +416,7 @@ class ClimateEntity(Entity): Requires SUPPORT_SWING_MODE. """ - raise NotImplementedError + return self._attr_swing_mode @property def swing_modes(self) -> list[str] | None: @@ -398,7 +424,7 @@ class ClimateEntity(Entity): Requires SUPPORT_SWING_MODE. """ - raise NotImplementedError + return self._attr_swing_modes def set_temperature(self, **kwargs) -> None: """Set new target temperature.""" @@ -494,31 +520,35 @@ class ClimateEntity(Entity): @property def supported_features(self) -> int: """Return the list of supported features.""" - raise NotImplementedError() + return self._attr_supported_features @property def min_temp(self) -> float: """Return the minimum temperature.""" - return convert_temperature( - DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit - ) + if not hasattr(self, "_attr_min_temp"): + return convert_temperature( + DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit + ) + return self._attr_min_temp @property def max_temp(self) -> float: """Return the maximum temperature.""" - return convert_temperature( - DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit - ) + if not hasattr(self, "_attr_max_temp"): + return convert_temperature( + DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit + ) + return self._attr_max_temp @property def min_humidity(self) -> int: """Return the minimum humidity.""" - return DEFAULT_MIN_HUMIDITY + return self._attr_min_humidity @property def max_humidity(self) -> int: """Return the maximum humidity.""" - return DEFAULT_MAX_HUMIDITY + return self._attr_max_humidity async def async_service_aux_heat( diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 1c7bde7d9e5..e69c178e595 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from . import ToonDataUpdateCoordinator from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN from .helpers import toon_exception_handler from .models import ToonDisplayDeviceEntity @@ -44,28 +45,31 @@ async def async_setup_entry( class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): """Representation of a Toon climate device.""" - @property - def unique_id(self) -> str: - """Return the unique ID for this thermostat.""" - agreement_id = self.coordinator.data.agreement.agreement_id - # This unique ID is a bit ugly and contains unneeded information. - # It is here for lecagy / backward compatible reasons. - return f"{DOMAIN}_{agreement_id}_climate" + _attr_hvac_mode = HVAC_MODE_HEAT + _attr_max_temp = DEFAULT_MAX_TEMP + _attr_min_temp = DEFAULT_MIN_TEMP + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + _attr_temperature_unit = TEMP_CELSIUS - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode.""" - return HVAC_MODE_HEAT - - @property - def hvac_modes(self) -> list[str]: - """Return the list of available hvac operation modes.""" - return [HVAC_MODE_HEAT] + def __init__( + self, + coordinator: ToonDataUpdateCoordinator, + *, + name: str, + icon: str, + ) -> None: + """Initialize Toon climate entity.""" + super().__init__(coordinator, name=name, icon=icon) + self._attr_hvac_modes = [HVAC_MODE_HEAT] + self._attr_preset_modes = [ + PRESET_AWAY, + PRESET_COMFORT, + PRESET_HOME, + PRESET_SLEEP, + ] + self._attr_unique_id = ( + f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_climate" + ) @property def hvac_action(self) -> str | None: @@ -74,11 +78,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" @@ -90,11 +89,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): } return mapping.get(self.coordinator.data.thermostat.active_state) - @property - def preset_modes(self) -> list[str]: - """Return a list of available preset modes.""" - return [PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_SLEEP] - @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -105,16 +99,6 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): """Return the temperature we try to reach.""" return self.coordinator.data.thermostat.current_setpoint - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - return DEFAULT_MIN_TEMP - - @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - return DEFAULT_MAX_TEMP - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the current state of the burner.""" From f7f8672eeacbaa4bba50b948b51fcfd7a858bbd7 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Thu, 27 May 2021 19:56:59 +0200 Subject: [PATCH 021/750] Add tests for sonos switch platform (#51142) * add tests * refactor async_added_to_hass * fix tests and race condition * use async_get * typo --- homeassistant/components/sonos/speaker.py | 2 - homeassistant/components/sonos/switch.py | 18 ++++++--- tests/components/sonos/conftest.py | 48 +++++++++++++++++++---- tests/components/sonos/test_switch.py | 35 +++++++++++++++-- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index c1f1dbb9104..bb6cb306426 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -420,8 +420,6 @@ class SonosSpeaker: async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) - self.async_write_entity_states() - async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 967bc21da59..879d5fa0a99 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -43,11 +43,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity = SonosAlarmEntity(alarm_id, speaker) async_add_entities([entity]) configured_alarms.add(alarm_id) - config_entry.async_on_unload( - async_dispatcher_connect( - hass, SONOS_ALARM_UPDATE, entity.async_update - ) - ) config_entry.async_on_unload( async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) @@ -64,9 +59,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self._alarm_id = alarm_id self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") + async def async_added_to_hass(self) -> None: + """Handle switch setup when added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SONOS_ALARM_UPDATE, + self.async_update, + ) + ) + @property def alarm(self): - """Return the ID of the alarm.""" + """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.alarm_id] @property diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2feb2b54896..aa14dcaa5cf 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -30,7 +30,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarmClock + music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock ): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( @@ -46,7 +46,7 @@ def soco_fixture( mock_soco.zoneGroupTopology = dummy_soco_service mock_soco.contentDirectory = dummy_soco_service mock_soco.deviceProperties = dummy_soco_service - mock_soco.alarmClock = alarmClock + mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True @@ -90,12 +90,28 @@ def music_library_fixture(): return music_library -@pytest.fixture(name="alarmClock") -def alarmClock_fixture(): +@pytest.fixture(name="alarm_clock") +def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarmClock = Mock() - alarmClock.subscribe = AsyncMock() - alarmClock.ListAlarms.return_value = { + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + " " + } + return alarm_clock + + +@pytest.fixture(name="alarm_clock_extended") +def alarm_clock_fixture_extended(): + """Create alarmClock fixture.""" + alarm_clock = Mock() + alarm_clock.subscribe = AsyncMock() + alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '' " " } - return alarmClock + return alarm_clock @pytest.fixture(name="speaker_info") @@ -141,3 +157,19 @@ def battery_event_fixture(soco): "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", } return SonosMockEvent(soco, variables) + + +@pytest.fixture(name="alarm_event") +def alarm_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "time_zone": "ffc40a000503000003000502ffc4", + "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", + "time_generation": "20000001", + "alarm_list_version": "RINCON_test", + "time_format": "INV", + "date_format": "INV", + "daily_index_refresh_time": None, + } + + return SonosMockEvent(soco, variables) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index c33c472ee27..d4448d22b32 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -9,17 +9,18 @@ from homeassistant.components.sonos.switch import ( ATTR_VOLUME, ) from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component async def setup_platform(hass, config_entry, config): - """Set up the media player platform for testing.""" + """Set up the switch platform for testing.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() -async def test_entity_registry(hass, config_entry, config, soco): +async def test_entity_registry(hass, config_entry, config): """Test sonos device with alarm registered in the device registry.""" await setup_platform(hass, config_entry, config) @@ -29,7 +30,7 @@ async def test_entity_registry(hass, config_entry, config, soco): assert "switch.sonos_alarm_14" in entity_registry.entities -async def test_alarm_attributes(hass, config_entry, config, soco): +async def test_alarm_attributes(hass, config_entry, config): """Test for correct sonos alarm state.""" await setup_platform(hass, config_entry, config) @@ -45,3 +46,31 @@ async def test_alarm_attributes(hass, config_entry, config, soco): assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25 assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + + +async def test_alarm_create_delete( + hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event +): + """Test for correct creation and deletion of alarms during runtime.""" + soco.alarmClock = alarm_clock_extended + + await setup_platform(hass, config_entry, config) + + subscription = alarm_clock_extended.subscribe.return_value + sub_callback = subscription.callback + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + entity_registry = async_get_entity_registry(hass) + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" in entity_registry.entities + + alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" not in entity_registry.entities From 02ac9a75b1382a43cbe105523d11b44c6fa7d015 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Thu, 27 May 2021 20:01:04 +0100 Subject: [PATCH 022/750] Bump pyroon to 0.0.37 (#51164) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 09fcaad5f1f..354117e8fe4 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.0.36"], + "requirements": ["roonapi==0.0.37"], "codeowners": ["@pavoni"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 989829b0e61..5bcd60f10b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2015,7 +2015,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rova rova==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 551de00ccc3..5ae205c1e23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1091,7 +1091,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 93ada0a675a1a2ca2a26bb895f11f80cfddbe490 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 28 May 2021 00:19:07 +0000 Subject: [PATCH 023/750] [ci skip] Translation update --- .../components/elgato/translations/nl.json | 2 +- .../homekit_controller/translations/nl.json | 2 ++ .../keenetic_ndms2/translations/nl.json | 5 ++++- .../meteoclimatic/translations/nl.json | 20 +++++++++++++++++++ .../components/motioneye/translations/ca.json | 4 ++++ .../components/motioneye/translations/nl.json | 4 ++++ .../components/samsungtv/translations/nl.json | 3 +++ .../components/sia/translations/en.json | 2 +- .../components/sia/translations/nl.json | 6 ++++++ .../components/sia/translations/zh-Hant.json | 2 +- .../totalconnect/translations/ca.json | 3 ++- .../totalconnect/translations/nl.json | 5 +++-- .../totalconnect/translations/zh-Hant.json | 5 +++-- 13 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/meteoclimatic/translations/nl.json diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json index 5fa47d30a10..1605f4577ee 100644 --- a/homeassistant/components/elgato/translations/nl.json +++ b/homeassistant/components/elgato/translations/nl.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "Wilt u de Elgato Key Light met serienummer `{serial_number}` toevoegen aan Home Assistant?", - "title": "Elgato Key Light apparaat ontdekt" + "title": "Elgato Light apparaat ontdekt" } } } diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 64b1d0802db..4312fdc033c 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Onjuiste HomeKit-code. Controleer het en probeer het opnieuw.", + "insecure_setup_code": "De gevraagde setup-code is onveilig vanwege de triviale aard ervan. Dit accessoire voldoet niet aan de basisbeveiligingsvereisten.", "max_peers_error": "Apparaat heeft geweigerd om koppelingen toe te voegen omdat het geen vrije koppelingsopslag heeft.", "pairing_failed": "Er deed zich een fout voor tijdens het koppelen met dit apparaat. Dit kan een tijdelijke storing zijn of uw apparaat wordt mogelijk momenteel niet ondersteund.", "unable_to_pair": "Kan niet koppelen, probeer het opnieuw.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Koppelen met onveilige setup-codes toestaan.", "pairing_code": "Koppelingscode" }, "description": "HomeKit Controller communiceert met {name} via het lokale netwerk met behulp van een beveiligde versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Voer uw HomeKit-koppelcode in (in de indeling XXX-XX-XXX) om dit accessoire te gebruiken. Deze code is meestal te vinden op het apparaat zelf of in de verpakking.", diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json index d2a85c8b059..3dd5b56c51d 100644 --- a/homeassistant/components/keenetic_ndms2/translations/nl.json +++ b/homeassistant/components/keenetic_ndms2/translations/nl.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "no_udn": "SSDP-ontdekkingsinformatie heeft geen UDN", + "not_keenetic_ndms2": "Ontdekt item is geen Keenetic router" }, "error": { "cannot_connect": "Kan geen verbinding maken" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/meteoclimatic/translations/nl.json b/homeassistant/components/meteoclimatic/translations/nl.json new file mode 100644 index 00000000000..0b4aa397276 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "unknown": "Onverwachte fout" + }, + "error": { + "not_found": "Geen apparaten gevonden op het netwerk" + }, + "step": { + "user": { + "data": { + "code": "Station code" + }, + "description": "Voer de code van het meteoklimatologische station in (bv. ESCAT43000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json index 65ce7e48781..8f11dba2802 100644 --- a/homeassistant/components/motioneye/translations/ca.json +++ b/homeassistant/components/motioneye/translations/ca.json @@ -11,6 +11,10 @@ "unknown": "Error inesperat" }, "step": { + "hassio_confirm": { + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el servei motionEye proporcionat pel complement: {addon}?", + "title": "motionEye via complement de Home Assistant" + }, "user": { "data": { "admin_password": "Contrasenya d'administrador", diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json index 07d8dc71a10..0fd3c7661eb 100644 --- a/homeassistant/components/motioneye/translations/nl.json +++ b/homeassistant/components/motioneye/translations/nl.json @@ -11,6 +11,10 @@ "unknown": "Onverwachte fout" }, "step": { + "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met de motionEye-service die wordt geleverd door de add-on: {addon}?", + "title": "motionEye via Home Assistant add-on" + }, "user": { "data": { "admin_password": "Admin Wachtwoord", diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index c64b8beca78..3a0508099a8 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -19,6 +19,9 @@ "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Na het indienen, accepteer binnen 30 seconden de pop-up op {device} om autorisatie toe te staan." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json index ff6781669dd..50a00a4bf23 100644 --- a/homeassistant/components/sia/translations/en.json +++ b/homeassistant/components/sia/translations/en.json @@ -4,7 +4,7 @@ "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", - "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", "invalid_zones": "There needs to be at least 1 zone.", "unknown": "Unexpected error" diff --git a/homeassistant/components/sia/translations/nl.json b/homeassistant/components/sia/translations/nl.json index 789106ead23..8afc0b88651 100644 --- a/homeassistant/components/sia/translations/nl.json +++ b/homeassistant/components/sia/translations/nl.json @@ -1,6 +1,12 @@ { "config": { "error": { + "invalid_account_format": "Het account is geen hex waarde, gebruik alleen 0-9 en A-F.", + "invalid_account_length": "Het account heeft niet de juiste lengte, het moet tussen de 3 en 16 karakters zijn.", + "invalid_key_format": "De sleutel is geen hex waarde, gebruik alleen 0-9 en A-F.", + "invalid_key_length": "De sleutel heeft niet de juiste lengte, het moeten 16, 24 of 32 hex karakters zijn.", + "invalid_ping": "Het ping-interval moet tussen 1 en 1440 minuten liggen.", + "invalid_zones": "Er moet minstens 1 zone zijn.", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json index 6ebf56aa049..6cd3c879656 100644 --- a/homeassistant/components/sia/translations/zh-Hant.json +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -4,7 +4,7 @@ "invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", "invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002", "invalid_key_format": "\u5bc6\u9470\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", - "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 14\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002", + "invalid_key_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u70ba 16\u300124 \u6216 32 \u500b\u5341\u516d\u9032\u4f4d\u5b57\u5143\u3002", "invalid_ping": "Ping \u9593\u8ddd\u5fc5\u9808\u70ba 1 \u81f3 1440 \u5206\u9418\u4e4b\u9593\u3002", "invalid_zones": "\u81f3\u5c11\u5fc5\u9808\u6709\u4e00\u500b\u5206\u5340\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index ce055082a21..9e2a6913fd4 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -11,7 +11,8 @@ "step": { "locations": { "data": { - "location": "Ubicaci\u00f3" + "location": "Ubicaci\u00f3", + "usercode": "Codi d'usuari" }, "description": "Introdueix el codi d'usuari de l'usuari en aquesta ubicaci\u00f3", "title": "Codis d'usuari d'ubicaci\u00f3" diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index de20d40bee6..0ec7bb52d88 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Locatie" + "location": "Locatie", + "usercode": "Gebruikerscode" }, - "description": "Voer de gebruikerscode voor deze gebruiker op deze locatie in", + "description": "Voer de gebruikerscode voor deze gebruiker in op locatie {location_id}", "title": "Locatie gebruikerscodes" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json index 96921baf007..eb739cb5e38 100644 --- a/homeassistant/components/totalconnect/translations/zh-Hant.json +++ b/homeassistant/components/totalconnect/translations/zh-Hant.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "\u5ea7\u6a19" + "location": "\u5ea7\u6a19", + "usercode": "\u4f7f\u7528\u8005\u4ee3\u78bc" }, - "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19\u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc", + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19 {location_id} \u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc", "title": "\u5ea7\u6a19\u4f7f\u7528\u8005\u4ee3\u78bc" }, "reauth_confirm": { From ca8d09e5e143f05ed6fb6d9941678cfeb8f41ccc Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 27 May 2021 21:57:35 -0400 Subject: [PATCH 024/750] Add zwave_js.multicast_set_value service (#51115) * Add zwave_js.multicast_set_value service * comment * Add test for multiple config entries validation * additional validation test * brevity * wrap schema in vol.Schema * Update homeassistant/components/zwave_js/services.py Co-authored-by: Martin Hjelmare * do node transform and multicast validation in schema validation * move poll value entity validation into schema validation, pass helper functions dev and ent reg instead of retrieving it every time * make validators nested functions since they don't neeed to be externally accessible * Update homeassistant/components/zwave_js/services.py Co-authored-by: Martin Hjelmare * Remove errant ALLOW_EXTRA Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/const.py | 5 + homeassistant/components/zwave_js/helpers.py | 38 ++- homeassistant/components/zwave_js/services.py | 226 ++++++++++++++---- .../components/zwave_js/services.yaml | 50 ++++ tests/components/zwave_js/test_services.py | 180 +++++++++++++- 6 files changed, 432 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 520495d5071..beebe2cc3f8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -362,7 +362,7 @@ async def async_setup_entry( # noqa: C901 entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False - services = ZWaveServices(hass, ent_reg) + services = ZWaveServices(hass, ent_reg, dev_reg) services.async_register() # Set up websocket API diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 629cd222bd4..d7717922d10 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -44,6 +44,8 @@ ATTR_DATA_TYPE = "data_type" ATTR_WAIT_FOR_RESULT = "wait_for_result" # service constants +ATTR_NODES = "nodes" + SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters" @@ -56,5 +58,8 @@ SERVICE_REFRESH_VALUE = "refresh_value" ATTR_REFRESH_ALL_VALUES = "refresh_all_values" SERVICE_SET_VALUE = "set_value" +SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" + +ATTR_BROADCAST = "broadcast" ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index beee7fefa30..81eae0fdc15 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -10,8 +10,14 @@ from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.helpers.device_registry import ( + DeviceRegistry, + async_get as async_get_dev_reg, +) +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + async_get as async_get_ent_reg, +) from .const import CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN @@ -60,13 +66,17 @@ def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str] @callback -def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode: +def async_get_node_from_device_id( + hass: HomeAssistant, device_id: str, dev_reg: DeviceRegistry | None = None +) -> ZwaveNode: """ Get node from a device ID. Raises ValueError if device is invalid or node can't be found. """ - device_entry = async_get_dev_reg(hass).async_get(device_id) + if not dev_reg: + dev_reg = async_get_dev_reg(hass) + device_entry = dev_reg.async_get(device_id) if not device_entry: raise ValueError("Device ID is not valid") @@ -111,21 +121,25 @@ def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveN @callback -def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode: +def async_get_node_from_entity_id( + hass: HomeAssistant, + entity_id: str, + ent_reg: EntityRegistry | None = None, + dev_reg: DeviceRegistry | None = None, +) -> ZwaveNode: """ Get node from an entity ID. Raises ValueError if entity is invalid. """ - entity_entry = async_get_ent_reg(hass).async_get(entity_id) + if not ent_reg: + ent_reg = async_get_ent_reg(hass) + entity_entry = ent_reg.async_get(entity_id) - if not entity_entry: - raise ValueError("Entity ID is not valid") - - if entity_entry.platform != DOMAIN: - raise ValueError("Entity is not from zwave_js integration") + if entity_entry is None or entity_entry.platform != DOMAIN: + raise ValueError(f"Entity {entity_id} is not a valid {DOMAIN} entity.") # Assert for mypy, safe because we know that zwave_js entities are always # tied to a device assert entity_entry.device_id - return async_get_node_from_device_id(hass, entity_entry.device_id) + return async_get_node_from_device_id(hass, entity_entry.device_id, dev_reg) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index c2ebe965fdd..65184abbd08 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -2,12 +2,15 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol +from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandStatus from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import get_value_id +from zwave_js_server.util.multicast import async_multicast_set_value from zwave_js_server.util.node import ( async_bulk_set_partial_config_parameters, async_set_config_parameter, @@ -16,6 +19,7 @@ from zwave_js_server.util.node import ( from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import EntityRegistry @@ -26,8 +30,8 @@ _LOGGER = logging.getLogger(__name__) def parameter_name_does_not_need_bitmask( - val: dict[str, int | str] -) -> dict[str, int | str]: + val: dict[str, int | str | list[str]] +) -> dict[str, int | str | list[str]]: """Validate that if a parameter name is provided, bitmask is not as well.""" if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and ( val.get(const.ATTR_CONFIG_PARAMETER_BITMASK) @@ -39,6 +43,16 @@ def parameter_name_does_not_need_bitmask( return val +def broadcast_command(val: dict[str, Any]) -> dict[str, Any]: + """Validate that the service call is for a broadcast command.""" + if val.get(const.ATTR_BROADCAST): + return val + raise vol.Invalid( + "Either `broadcast` must be set to True or multiple devices/entities must be " + "specified" + ) + + # Validates that a bitmask is provided in hex form and converts it to decimal # int equivalent since that's what the library uses BITMASK_SCHEMA = vol.All( @@ -55,14 +69,95 @@ BITMASK_SCHEMA = vol.All( class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" - def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry) -> None: + def __init__( + self, hass: HomeAssistant, ent_reg: EntityRegistry, dev_reg: DeviceRegistry + ) -> None: """Initialize with hass object.""" self._hass = hass self._ent_reg = ent_reg + self._dev_reg = dev_reg @callback def async_register(self) -> None: """Register all our services.""" + + @callback + def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: + """Get nodes set from service data.""" + nodes: set[ZwaveNode] = set() + try: + if ATTR_ENTITY_ID in val: + nodes |= { + async_get_node_from_entity_id( + self._hass, entity_id, self._ent_reg, self._dev_reg + ) + for entity_id in val[ATTR_ENTITY_ID] + } + val.pop(ATTR_ENTITY_ID) + if ATTR_DEVICE_ID in val: + nodes |= { + async_get_node_from_device_id( + self._hass, device_id, self._dev_reg + ) + for device_id in val[ATTR_DEVICE_ID] + } + val.pop(ATTR_DEVICE_ID) + except ValueError as err: + raise vol.Invalid(err.args[0]) from err + + val[const.ATTR_NODES] = nodes + return val + + @callback + def validate_multicast_nodes(val: dict[str, Any]) -> dict[str, Any]: + """Validate the input nodes for multicast.""" + nodes: set[ZwaveNode] = val[const.ATTR_NODES] + broadcast: bool = val[const.ATTR_BROADCAST] + + # User must specify a node if they are attempting a broadcast and have more + # than one zwave-js network. We know it's a broadcast if the nodes list is + # empty because of schema validation. + if ( + not nodes + and len(self._hass.config_entries.async_entries(const.DOMAIN)) > 1 + ): + raise vol.Invalid( + "You must include at least one entity or device in the service call" + ) + + # When multicasting, user must specify at least two nodes + if not broadcast and len(nodes) < 2: + raise vol.Invalid( + "To set a value on a single node, use the zwave_js.set_value service" + ) + + first_node = next((node for node in nodes), None) + + # If any nodes don't have matching home IDs, we can't run the command because + # we can't multicast across multiple networks + if first_node and any( + node.client.driver.controller.home_id + != first_node.client.driver.controller.home_id + for node in nodes + ): + raise vol.Invalid( + "Multicast commands only work on devices in the same network" + ) + + return val + + @callback + def validate_entities(val: dict[str, Any]) -> dict[str, Any]: + """Validate entities exist and are from the zwave_js platform.""" + for entity_id in val[ATTR_ENTITY_ID]: + entry = self._ent_reg.async_get(entity_id) + if entry is None or entry.platform != const.DOMAIN: + raise vol.Invalid( + f"Entity {entity_id} is not a valid {const.DOMAIN} entity." + ) + + return val + self._hass.services.async_register( const.DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, @@ -86,6 +181,7 @@ class ZWaveServices: }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), parameter_name_does_not_need_bitmask, + get_nodes_from_service_data, ), ), ) @@ -112,6 +208,7 @@ class ZWaveServices: ), }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + get_nodes_from_service_data, ), ), ) @@ -121,10 +218,15 @@ class ZWaveServices: const.SERVICE_REFRESH_VALUE, self.async_poll_value, schema=vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(const.ATTR_REFRESH_ALL_VALUES, default=False): bool, - } + vol.All( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional( + const.ATTR_REFRESH_ALL_VALUES, default=False + ): bool, + }, + validate_entities, + ) ), ) @@ -153,23 +255,48 @@ class ZWaveServices: vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + get_nodes_from_service_data, + ), + ), + ) + + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_MULTICAST_SET_VALUE, + self.async_multicast_set_value, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(const.ATTR_BROADCAST, default=False): cv.boolean, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + }, + vol.Any( + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + broadcast_command, + ), + get_nodes_from_service_data, + validate_multicast_nodes, ), ), ) async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" - nodes: set[ZwaveNode] = set() - if ATTR_ENTITY_ID in service.data: - nodes |= { - async_get_node_from_entity_id(self._hass, entity_id) - for entity_id in service.data[ATTR_ENTITY_ID] - } - if ATTR_DEVICE_ID in service.data: - nodes |= { - async_get_node_from_device_id(self._hass, device_id) - for device_id in service.data[ATTR_DEVICE_ID] - } + nodes = service.data[const.ATTR_NODES] property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER] property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -196,17 +323,7 @@ class ZWaveServices: self, service: ServiceCall ) -> None: """Bulk set multiple partial config values on a node.""" - nodes: set[ZwaveNode] = set() - if ATTR_ENTITY_ID in service.data: - nodes |= { - async_get_node_from_entity_id(self._hass, entity_id) - for entity_id in service.data[ATTR_ENTITY_ID] - } - if ATTR_DEVICE_ID in service.data: - nodes |= { - async_get_node_from_device_id(self._hass, device_id) - for device_id in service.data[ATTR_DEVICE_ID] - } + nodes = service.data[const.ATTR_NODES] property_ = service.data[const.ATTR_CONFIG_PARAMETER] new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -231,10 +348,7 @@ class ZWaveServices: """Poll value on a node.""" for entity_id in service.data[ATTR_ENTITY_ID]: entry = self._ent_reg.async_get(entity_id) - if entry is None or entry.platform != const.DOMAIN: - raise ValueError( - f"Entity {entity_id} is not a valid {const.DOMAIN} entity." - ) + assert entry # Schema validation would have failed if we can't do this async_dispatcher_send( self._hass, f"{const.DOMAIN}_{entry.unique_id}_poll_value", @@ -243,17 +357,7 @@ class ZWaveServices: async def async_set_value(self, service: ServiceCall) -> None: """Set a value on a node.""" - nodes: set[ZwaveNode] = set() - if ATTR_ENTITY_ID in service.data: - nodes |= { - async_get_node_from_entity_id(self._hass, entity_id) - for entity_id in service.data[ATTR_ENTITY_ID] - } - if ATTR_DEVICE_ID in service.data: - nodes |= { - async_get_node_from_device_id(self._hass, device_id) - for device_id in service.data[ATTR_DEVICE_ID] - } + nodes = service.data[const.ATTR_NODES] command_class = service.data[const.ATTR_COMMAND_CLASS] property_ = service.data[const.ATTR_PROPERTY] property_key = service.data.get(const.ATTR_PROPERTY_KEY) @@ -280,3 +384,37 @@ class ZWaveServices: "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue " "for possible reasons" ) + + async def async_multicast_set_value(self, service: ServiceCall) -> None: + """Set a value via multicast to multiple nodes.""" + nodes = service.data[const.ATTR_NODES] + broadcast: bool = service.data[const.ATTR_BROADCAST] + + value = { + "commandClass": service.data[const.ATTR_COMMAND_CLASS], + "property": service.data[const.ATTR_PROPERTY], + "propertyKey": service.data.get(const.ATTR_PROPERTY_KEY), + "endpoint": service.data.get(const.ATTR_ENDPOINT), + } + new_value = service.data[const.ATTR_VALUE] + + # If there are no nodes, we can assume there is only one config entry due to + # schema validation and can use that to get the client, otherwise we can just + # get the client from the node. + client: ZwaveClient = None + first_node = next((node for node in nodes), None) + if first_node: + client = first_node.client + else: + entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id + client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] + + success = await async_multicast_set_value( + client, + new_value, + {k: v for k, v in value.items() if v is not None}, + None if broadcast else list(nodes), + ) + + if success is False: + raise SetValueFailed("Unable to set value via multicast") diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 84877189298..16be02b7f1b 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -160,3 +160,53 @@ set_value: required: false selector: boolean: + +multicast_set_value: + name: Set a value on multiple Z-Wave devices via multicast (Advanced) + description: Allow for changing any value that Z-Wave JS recognizes on multiple Z-Wave devices using multicast, so all devices receive the message simultaneously. This service has minimal validation so only use this service if you know what you are doing. + target: + entity: + integration: zwave_js + fields: + broadcast: + name: Broadcast? + description: Whether command should be broadcast to all devices on the networrk. + example: true + required: false + selector: + boolean: + command_class: + name: Command Class + description: The ID of the command class for the value. + example: 117 + required: true + selector: + text: + endpoint: + name: Endpoint + description: The endpoint for the value. + example: 1 + required: false + selector: + text: + property: + name: Property + description: The ID of the property for the value. + example: currentValue + required: true + selector: + text: + property_key: + name: Property Key + description: The ID of the property key for the value + example: 1 + required: false + selector: + text: + value: + name: Value + description: The new value to set. + example: "ffbb99" + required: true + selector: + object: diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 3c08c49a36f..4f70543d3e2 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1,9 +1,12 @@ """Test the Z-Wave JS services.""" +from unittest.mock import MagicMock, patch + import pytest import voluptuous as vol from zwave_js_server.exceptions import SetValueFailed from homeassistant.components.zwave_js.const import ( + ATTR_BROADCAST, ATTR_COMMAND_CLASS, ATTR_CONFIG_PARAMETER, ATTR_CONFIG_PARAMETER_BITMASK, @@ -14,6 +17,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_WAIT_FOR_RESULT, DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + SERVICE_MULTICAST_SET_VALUE, SERVICE_REFRESH_VALUE, SERVICE_SET_CONFIG_PARAMETER, SERVICE_SET_VALUE, @@ -212,8 +216,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): } assert args["value"] == 1 - # Test that an invalid entity ID raises a ValueError - with pytest.raises(ValueError): + # Test that an invalid entity ID raises a MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -225,8 +229,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): blocking=True, ) - # Test that an invalid device ID raises a ValueError - with pytest.raises(ValueError): + # Test that an invalid device ID raises a MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -259,8 +263,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): identifiers={("test", "test")}, ) - # Test that a non Z-Wave JS device raises a ValueError - with pytest.raises(ValueError): + # Test that a non Z-Wave JS device raises a MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -276,8 +280,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): config_entry_id=integration.entry_id, identifiers={(DOMAIN, "500-500")} ) - # Test that a Z-Wave JS device with an invalid node ID raises a ValueError - with pytest.raises(ValueError): + # Test that a Z-Wave JS device with an invalid node ID raises a MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -297,8 +301,8 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): config_entry=non_zwave_js_config_entry, ) - # Test that a non Z-Wave JS entity raises a ValueError - with pytest.raises(ValueError): + # Test that a non Z-Wave JS entity raises a MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -530,8 +534,8 @@ async def test_poll_value( ) assert len(client.async_send_command.call_args_list) == 8 - # Test polling against an invalid entity raises ValueError - with pytest.raises(ValueError): + # Test polling against an invalid entity raises MultipleInvalid + with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_REFRESH_VALUE, @@ -634,3 +638,155 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): }, blocking=True, ) + + +async def test_multicast_set_value( + hass, + client, + climate_danfoss_lc_13, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test multicast_set_value service.""" + # Test successful multicast call + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "multicast_group.set_value" + assert args["nodeIDs"] == [ + climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + climate_danfoss_lc_13.node_id, + ] + assert args["valueId"] == { + "commandClass": 117, + "property": "local", + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + + # Test successful broadcast call + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_BROADCAST: True, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "broadcast_node.set_value" + assert args["valueId"] == { + "commandClass": 117, + "property": "local", + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + + # Test sending one node without broadcast fails + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + # Test no device, entity, or broadcast flag raises error + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + # Test that when a command fails we raise an exception + client.async_send_command.return_value = {"success": False} + + with pytest.raises(SetValueFailed): + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + # Create a fake node with a different home ID from a real node and patch it into + # return of helper function to check the validation for two nodes having different + # home IDs + diff_network_node = MagicMock() + diff_network_node.client.driver.controller.home_id.return_value = "diff_home_id" + + with pytest.raises(vol.MultipleInvalid), patch( + "homeassistant.components.zwave_js.services.async_get_node_from_device_id", + return_value=diff_network_node, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + ], + ATTR_DEVICE_ID: "fake_device_id", + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) + + # Test that when there are multiple zwave_js config entries, service will fail + # without devices or entities + new_entry = MockConfigEntry(domain=DOMAIN) + new_entry.add_to_hass(hass) + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_BROADCAST: True, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + }, + blocking=True, + ) From e08de2273790ae4b9a39c2c0f47bce5a77345b1e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 27 May 2021 21:30:37 -0500 Subject: [PATCH 025/750] Fix totalconnect test calling public host (#51138) --- tests/components/totalconnect/test_config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 3751abfc361..7b80996db14 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -135,15 +135,14 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.totalconnect.config_flow.TotalConnectClient.TotalConnectClient" - ) as client_mock: + ) as client_mock, patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ): # first test with an invalid password client_mock.return_value.is_valid_credentials.return_value = False @@ -162,5 +161,6 @@ async def test_reauth(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" + await hass.async_block_till_done() assert len(hass.config_entries.async_entries()) == 1 From 6ad29aec2c77d21d2dd6721324997ee42fb8ace6 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 28 May 2021 13:36:41 +0800 Subject: [PATCH 026/750] Adjust segment duration calculation in stream (#51149) * Calculate min segment duration internally * Rename segments to sequences in StreamOutput * Fix segment duration calculation in test_worker --- homeassistant/components/stream/const.py | 6 +++- homeassistant/components/stream/core.py | 2 +- homeassistant/components/stream/hls.py | 6 ++-- tests/components/stream/test_worker.py | 41 ++++++++++++++++-------- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index e1b1b610e03..20ff8210996 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -19,7 +19,11 @@ OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist MAX_SEGMENTS = 4 # Max number of segments to keep around -MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds +TARGET_SEGMENT_DURATION = 2.0 # Each segment is about this many seconds +SEGMENT_DURATION_ADJUSTER = 0.1 # Used to avoid missing keyframe boundaries +MIN_SEGMENT_DURATION = ( + TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER +) # Each segment is at least this many seconds PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 70cd5b2eba8..76fae3cdacf 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -105,7 +105,7 @@ class StreamOutput: return -1 @property - def segments(self) -> list[int]: + def sequences(self) -> list[int]: """Return current sequence from segments.""" return [s.sequence for s in self._segments] diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 4968c935d72..7b5185da6bf 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -36,7 +36,7 @@ class HlsMasterPlaylistView(StreamView): # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work # Calculate file size / duration and use a small multiplier to account for variation # hls spec already allows for 25% variation - segment = track.get_segment(track.segments[-1]) + segment = track.get_segment(track.sequences[-1]) bandwidth = round( (len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2 ) @@ -53,7 +53,7 @@ class HlsMasterPlaylistView(StreamView): track = stream.add_provider(HLS_PROVIDER) stream.start() # Wait for a segment to be ready - if not track.segments and not await track.recv(): + if not track.sequences and not await track.recv(): return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -112,7 +112,7 @@ class HlsPlaylistView(StreamView): track = stream.add_provider(HLS_PROVIDER) stream.start() # Wait for a segment to be ready - if not track.segments and not await track.recv(): + if not track.sequences and not await track.recv(): return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 501ea302172..aa354ef41cb 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -25,8 +25,8 @@ from homeassistant.components.stream import Stream from homeassistant.components.stream.const import ( HLS_PROVIDER, MAX_MISSING_DTS, - MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, + TARGET_SEGMENT_DURATION, ) from homeassistant.components.stream.worker import SegmentBuffer, stream_worker @@ -36,9 +36,10 @@ AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 AUDIO_SAMPLE_RATE = 11025 +KEYFRAME_INTERVAL = 1 # in seconds PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds SEGMENT_DURATION = ( - math.ceil(MIN_SEGMENT_DURATION / PACKET_DURATION) * PACKET_DURATION + math.ceil(TARGET_SEGMENT_DURATION / KEYFRAME_INTERVAL) * KEYFRAME_INTERVAL ) # in seconds TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE @@ -102,7 +103,8 @@ class PacketSequence: pts = self.packet * PACKET_DURATION / time_base duration = PACKET_DURATION / time_base stream = VIDEO_STREAM - is_keyframe = True + # Pretend we get 1 keyframe every second + is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) size = 3 return FakePacket() @@ -247,8 +249,13 @@ async def test_stream_worker_success(hass): async def test_skip_out_of_order_packet(hass): """Skip a single out of order packet.""" packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) + # for this test, make sure the out of order index doesn't happen on a keyframe + out_of_order_index = OUT_OF_ORDER_PACKET_INDEX + if packets[out_of_order_index].is_keyframe: + out_of_order_index += 1 # This packet is out of order - packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090 + assert not packets[out_of_order_index].is_keyframe + packets[out_of_order_index].dts = -9090 decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments @@ -257,11 +264,9 @@ async def test_skip_out_of_order_packet(hass): # If skipped packet would have been the first packet of a segment, the previous # segment will be longer by a packet duration # We also may possibly lose a segment due to the shifting pts boundary - if OUT_OF_ORDER_PACKET_INDEX % PACKETS_PER_SEGMENT == 0: + if out_of_order_index % PACKETS_PER_SEGMENT == 0: # Check duration of affected segment and remove it - longer_segment_index = int( - (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET - ) + longer_segment_index = int((out_of_order_index - 1) * SEGMENTS_PER_PACKET) assert ( segments[longer_segment_index].duration == SEGMENT_DURATION + PACKET_DURATION @@ -327,15 +332,21 @@ async def test_skip_initial_bad_packets(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments - # Check number of segments - assert len(segments) == int( - (num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET - ) # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations assert all(s.duration == SEGMENT_DURATION for s in segments) - assert len(decoded_stream.video_packets) == num_packets - num_bad_packets + assert ( + len(decoded_stream.video_packets) + == num_packets + - math.ceil(num_bad_packets / (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL)) + * VIDEO_FRAME_RATE + * KEYFRAME_INTERVAL + ) + # Check number of segments + assert len(segments) == int( + (len(decoded_stream.video_packets) - 1) * SEGMENTS_PER_PACKET + ) assert len(decoded_stream.audio_packets) == 0 @@ -363,6 +374,9 @@ async def test_skip_missing_dts(hass): bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2) num_bad_packets = MAX_MISSING_DTS - 1 for i in range(bad_packet_start, bad_packet_start + num_bad_packets): + if packets[i].is_keyframe: + num_bad_packets -= 1 + continue packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) @@ -450,6 +464,7 @@ async def test_audio_is_first_packet(hass): packets[0].stream = AUDIO_STREAM packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1 packets[2].stream = AUDIO_STREAM packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE From eb66f8ef6d785d476b7c68fd9f5d06a94b8c231c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 08:00:11 +0200 Subject: [PATCH 027/750] Fix Netatmo data class update (#51177) --- homeassistant/components/netatmo/data_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 83215bd3af5..11415417ee1 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -99,7 +99,8 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - await self.async_fetch_data(data_class["name"]) + if data_class_name := data_class["name"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) From e9b09325c947f47eab02cd8ce0e9cdd1c9bf3837 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 08:24:55 +0200 Subject: [PATCH 028/750] Use entity class vars in SolarEdge (#51123) --- homeassistant/components/solaredge/sensor.py | 43 ++++---------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index b19705edbd3..75bb25f722d 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -127,20 +127,9 @@ class SolarEdgeSensor(CoordinatorEntity, SensorEntity): self.sensor_key = sensor_key self.data_service = data_service - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return SENSOR_TYPES[self.sensor_key][2] - - @property - def name(self) -> str: - """Return the name.""" - return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" - - @property - def icon(self) -> str | None: - """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][3] + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_key][2] + self._attr_name = f"{platform_name} ({SENSOR_TYPES[sensor_key][1]})" + self._attr_icon = SENSOR_TYPES[sensor_key][3] class SolarEdgeOverviewSensor(SolarEdgeSensor): @@ -202,6 +191,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] + self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -213,15 +203,12 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self.data_service.unit - class SolarEdgePowerFlowSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API power flow sensor.""" + _attr_device_class = DEVICE_CLASS_POWER + def __init__( self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService ) -> None: @@ -229,11 +216,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] - - @property - def device_class(self) -> str: - """Device Class.""" - return DEVICE_CLASS_POWER + self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: @@ -245,15 +228,12 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): """Return the state of the sensor.""" return self.data_service.data.get(self._json_key) - @property - def unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self.data_service.unit - class SolarEdgeStorageLevelSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API storage level sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY + def __init__( self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService ) -> None: @@ -262,11 +242,6 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] - @property - def device_class(self) -> str: - """Return the device_class of the device.""" - return DEVICE_CLASS_BATTERY - @property def state(self) -> str | None: """Return the state of the sensor.""" From 0b15f3aa989b5d05526928d224ec254b7ef9686a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 08:29:01 +0200 Subject: [PATCH 029/750] Define alarm_control_panel entity attributes as class variables (#51120) * Define alarm_control_panel entity attributes as class variables * Example Verisure * Remove redundant AttributeError --- .../alarm_control_panel/__init__.py | 14 ++++++++----- .../verisure/alarm_control_panel.py | 20 +++---------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 2d6d1f4d5b1..c8da648fec6 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,7 +1,6 @@ """Component to interface with an alarm control panel.""" from __future__ import annotations -from abc import abstractmethod from datetime import timedelta import logging from typing import Any, Final, final @@ -113,20 +112,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class AlarmControlPanelEntity(Entity): """An abstract class for alarm control entities.""" + _attr_changed_by: str | None = None + _attr_code_arm_required: bool = True + _attr_code_format: str | None = None + _attr_supported_features: int + @property def code_format(self) -> str | None: """Regex for code format or None if no code is required.""" - return None + return self._attr_code_format @property def changed_by(self) -> str | None: """Last change triggered by.""" - return None + return self._attr_changed_by @property def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" - return True + return self._attr_code_arm_required def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -177,9 +181,9 @@ class AlarmControlPanelEntity(Entity): await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) @property - @abstractmethod def supported_features(self) -> int: """Return the list of supported features.""" + return self._attr_supported_features @final @property diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 4def470ac5e..176ca9444c1 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -35,8 +35,9 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): coordinator: VerisureDataUpdateCoordinator + _attr_code_format = FORMAT_NUMBER _attr_name = "Verisure Alarm" - _changed_by: str | None = None + _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY @property def device_info(self) -> DeviceInfo: @@ -48,26 +49,11 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, } - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - @property def unique_id(self) -> str: """Return the unique ID for this entity.""" return self.coordinator.entry.data[CONF_GIID] - @property - def code_format(self) -> str: - """Return one or more digits/characters.""" - return FORMAT_NUMBER - - @property - def changed_by(self) -> str | None: - """Return the last change triggered by.""" - return self._changed_by - async def _async_set_arm_state(self, state: str, code: str | None = None) -> None: """Send set arm state command.""" arm_state = await self.hass.async_add_executor_job( @@ -102,7 +88,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): self._attr_state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) - self._changed_by = self.coordinator.data["alarm"].get("name") + self._attr_changed_by = self.coordinator.data["alarm"].get("name") super()._handle_coordinator_update() async def async_added_to_hass(self) -> None: From e41fbdc9eb94fa60fa2f3a496b652f952ff21a7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 May 2021 10:42:22 +0200 Subject: [PATCH 030/750] Bump actions/cache from 2.1.5 to 2.1.6 (#51185) Bumps [actions/cache](https://github.com/actions/cache) from 2.1.5 to 2.1.6. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.5...v2.1.6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe14e44beed..f6518f67ce2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: >- @@ -65,7 +65,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -92,7 +92,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -104,7 +104,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -132,7 +132,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -144,7 +144,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -172,7 +172,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -184,7 +184,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -234,7 +234,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -246,7 +246,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -277,7 +277,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -289,7 +289,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -320,7 +320,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -332,7 +332,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -360,7 +360,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -372,7 +372,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -403,7 +403,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -415,7 +415,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -454,7 +454,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -466,7 +466,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -496,7 +496,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -525,7 +525,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -561,7 +561,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: >- @@ -598,7 +598,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -629,7 +629,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -663,7 +663,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -721,7 +721,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From 837220cce40890e296920d33a623adbc11bd15a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 11:01:28 +0200 Subject: [PATCH 031/750] Add deprecated backwards compatible history.LazyState (#51144) --- homeassistant/components/history/__init__.py | 20 +++--- homeassistant/helpers/deprecation.py | 72 +++++++++++++------- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index c92718a87e4..ac8a13e69ad 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -13,8 +13,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import history -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder import history, models as history_models from homeassistant.components.recorder.statistics import statistics_during_period from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( @@ -26,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import deprecated_function +from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -110,6 +109,11 @@ async def async_setup(hass, config): return True +@deprecated_class("homeassistant.components.recorder.models.LazyState") +class LazyState(history_models.LazyState): + """A lazy version of core State.""" + + @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -345,17 +349,17 @@ class Filters: """Generate the entity filter query.""" includes = [] if self.included_domains: - includes.append(States.domain.in_(self.included_domains)) + includes.append(history_models.States.domain.in_(self.included_domains)) if self.included_entities: - includes.append(States.entity_id.in_(self.included_entities)) + includes.append(history_models.States.entity_id.in_(self.included_entities)) for glob in self.included_entity_globs: includes.append(_glob_to_like(glob)) excludes = [] if self.excluded_domains: - excludes.append(States.domain.in_(self.excluded_domains)) + excludes.append(history_models.States.domain.in_(self.excluded_domains)) if self.excluded_entities: - excludes.append(States.entity_id.in_(self.excluded_entities)) + excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) for glob in self.excluded_entity_globs: excludes.append(_glob_to_like(glob)) @@ -373,7 +377,7 @@ class Filters: def _glob_to_like(glob_str): """Translate glob to sql.""" - return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) + return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) def _entities_may_have_state_changes_after( diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 06f09327dc9..adf3d8a5d88 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -80,6 +80,23 @@ def get_deprecated( return config.get(new_name, default) +def deprecated_class(replacement: str) -> Any: + """Mark class as deprecated and provide a replacement class to be used instead.""" + + def deprecated_decorator(cls: Any) -> Any: + """Decorate class as deprecated.""" + + @functools.wraps(cls) + def deprecated_cls(*args: tuple, **kwargs: dict[str, Any]) -> Any: + """Wrap for the original class.""" + _print_deprecation_warning(cls, replacement, "class") + return cls(*args, **kwargs) + + return deprecated_cls + + return deprecated_decorator + + def deprecated_function(replacement: str) -> Callable[..., Callable]: """Mark function as deprecated and provide a replacement function to be used instead.""" @@ -89,32 +106,39 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: @functools.wraps(func) def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any: """Wrap for the original function.""" - logger = logging.getLogger(func.__module__) - try: - _, integration, path = get_integration_frame() - if path == "custom_components/": - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", - func.__name__, - integration, - replacement, - integration, - ) - else: - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead", - func.__name__, - integration, - replacement, - ) - except MissingIntegrationFrame: - logger.warning( - "%s is a deprecated function. Use %s instead", - func.__name__, - replacement, - ) + _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) return deprecated_func return deprecated_decorator + + +def _print_deprecation_warning(obj: Any, replacement: str, description: str) -> None: + logger = logging.getLogger(obj.__module__) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead, please report this to the maintainer of %s", + obj.__name__, + integration, + description, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead", + obj.__name__, + integration, + description, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated %s. Use %s instead", + obj.__name__, + description, + replacement, + ) From 538a03ee0c97bc6a1da0749660e0c75e016e2150 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 11:10:01 +0200 Subject: [PATCH 032/750] Clean up Speedtest.net Sensors (#51124) --- .../components/speedtestdotnet/sensor.py | 95 +++++++++---------- 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index c49a5691cec..e28aa0b2527 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -1,7 +1,14 @@ """Support for Speedtest.net internet speed testing sensor.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.sensor import SensorEntity +from homeassistant.components.speedtestdotnet import SpeedTestDataCoordinator +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -19,82 +26,64 @@ from .const import ( ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Speedtestdotnet sensors.""" - speedtest_coordinator = hass.data[DOMAIN] - - entities = [] - for sensor_type in SENSOR_TYPES: - entities.append(SpeedtestSensor(speedtest_coordinator, sensor_type)) - - async_add_entities(entities) + async_add_entities( + SpeedtestSensor(speedtest_coordinator, sensor_type) + for sensor_type in SENSOR_TYPES + ) class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Implementation of a speedtest.net sensor.""" - def __init__(self, coordinator, sensor_type): + coordinator: SpeedTestDataCoordinator + + _attr_icon = ICON + + def __init__(self, coordinator: SpeedTestDataCoordinator, sensor_type: str) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type - self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._state = None + + self._attr_name = f"{DEFAULT_NAME} {SENSOR_TYPES[sensor_type][0]}" + self._attr_unit_of_measurement = SENSOR_TYPES[self.type][1] + self._attr_unique_id = sensor_type @property - def name(self): - """Return the name of the sensor.""" - return f"{DEFAULT_NAME} {self._name}" - - @property - def unique_id(self): - """Return sensor unique_id.""" - return self.type - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" if not self.coordinator.data: return None + attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_SERVER_NAME: self.coordinator.data["server"]["name"], ATTR_SERVER_COUNTRY: self.coordinator.data["server"]["country"], ATTR_SERVER_ID: self.coordinator.data["server"]["id"], } + if self.type == "download": attributes[ATTR_BYTES_RECEIVED] = self.coordinator.data["bytes_received"] - - if self.type == "upload": + elif self.type == "upload": attributes[ATTR_BYTES_SENT] = self.coordinator.data["bytes_sent"] return attributes - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state: - self._state = state.state + self._attr_state = state.state @callback - def update(): + def update() -> None: """Update state.""" self._update_state() self.async_write_ha_state() @@ -102,12 +91,14 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self.async_on_remove(self.coordinator.async_add_listener(update)) self._update_state() - def _update_state(self): + def _update_state(self) -> None: """Update sensors state.""" - if self.coordinator.data: - if self.type == "ping": - self._state = self.coordinator.data["ping"] - elif self.type == "download": - self._state = round(self.coordinator.data["download"] / 10 ** 6, 2) - elif self.type == "upload": - self._state = round(self.coordinator.data["upload"] / 10 ** 6, 2) + if not self.coordinator.data: + return + + if self.type == "ping": + self._attr_state = self.coordinator.data["ping"] + elif self.type == "download": + self._attr_state = round(self.coordinator.data["download"] / 10 ** 6, 2) + elif self.type == "upload": + self._attr_state = round(self.coordinator.data["upload"] / 10 ** 6, 2) From d200f1e5048184a10c5442a425e6b34c7c606e71 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 28 May 2021 11:12:46 +0200 Subject: [PATCH 033/750] Bump config version to 2 for AVM Fritz Tools (#51176) --- homeassistant/components/fritz/__init__.py | 20 +++++++++++++++++++ homeassistant/components/fritz/config_flow.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 35e924c807c..b310d44a0f9 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,6 +3,10 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -63,6 +67,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate config entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + config_entry.version = 2 + hass.config_entries.async_update_entry( + config_entry, + options={CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds()}, + ) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FRITZ!Box Tools config entry.""" fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 4001dcadc71..9d3c9044765 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -38,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a FRITZ!Box Tools config flow.""" - VERSION = 1 + VERSION = 2 @staticmethod @callback From 47f016b340fd386bab0b77ce2a42564f86c11ac2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 May 2021 11:29:37 +0200 Subject: [PATCH 034/750] Remove old config from cover, including tests (#51118) * Remove old config and standardize new config. * Add missing safeguard. --- homeassistant/components/modbus/cover.py | 7 +----- homeassistant/components/modbus/fan.py | 2 +- homeassistant/components/modbus/light.py | 3 ++- homeassistant/components/modbus/switch.py | 3 +++ tests/components/modbus/test_cover.py | 30 ----------------------- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index ca00576770e..2317bc5401f 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -49,12 +49,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ): """Read configuration and create Modbus cover.""" - if discovery_info is None: - _LOGGER.warning( - "You're trying to init Modbus Cover in an unsupported way." - " Check https://www.home-assistant.io/integrations/modbus/#configuring-platform-cover" - " and fix your configuration" - ) + if discovery_info is None: # pragma: no cover return covers = [] diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 7fabff25711..a4d4265846d 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -20,7 +20,7 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Read configuration and create Modbus fans.""" - if discovery_info is None: + if discovery_info is None: # pragma: no cover return fans = [] diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index f56b01ff001..3eae5ed3db3 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -20,8 +20,9 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Read configuration and create Modbus lights.""" - if discovery_info is None: + if discovery_info is None: # pragma: no cover return + lights = [] for entry in discovery_info[CONF_LIGHTS]: hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 98e15d5b311..820e43419a0 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -22,6 +22,9 @@ async def async_setup_platform( """Read configuration and create Modbus switches.""" switches = [] + if discovery_info is None: # pragma: no cover + return + for entry in discovery_info[CONF_SWITCHES]: hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] switches.append(ModbusSwitch(hub, entry)) diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 8fbb45fde8e..4073e46a86e 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -1,5 +1,4 @@ """The tests for the Modbus cover component.""" -import logging from pymodbus.exceptions import ModbusException import pytest @@ -158,35 +157,6 @@ async def test_register_cover(hass, regs, expected): assert state == expected -@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) -async def test_unsupported_config_cover(hass, read_type, caplog): - """ - Run test for cover. - - Initialize the Cover in the legacy manner via platform. - This test expects that the Cover won't be initialized, and that we get a config warning. - """ - device_name = "test_cover" - device_config = {CONF_NAME: device_name, read_type: 1234} - - caplog.set_level(logging.WARNING) - caplog.clear() - - await base_config_test( - hass, - device_config, - device_name, - COVER_DOMAIN, - CONF_COVERS, - None, - method_discovery=False, - expect_init_to_fail=True, - ) - - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" - - async def test_service_cover_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" From 5afd16ef5d45150e160f488e2e60f9de7ab521b6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 May 2021 11:38:31 +0200 Subject: [PATCH 035/750] Move modbus schema validators to validators.py (#51121) --- homeassistant/components/modbus/__init__.py | 68 ++-------------- homeassistant/components/modbus/validators.py | 79 +++++++++++++++++-- tests/components/modbus/test_init.py | 8 +- 3 files changed, 84 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index b9765f5e5ee..d4b70796c1a 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol @@ -99,71 +98,20 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, - MINIMUM_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, - PLATFORMS, ) from .modbus import async_modbus_setup -from .validators import sensor_schema_validator +from .validators import ( + number_validator, + scan_interval_validator, + sensor_schema_validator, +) _LOGGER = logging.getLogger(__name__) BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) -def number(value: Any) -> int | float: - """Coerce a value to number without losing precision.""" - if isinstance(value, int): - return value - if isinstance(value, float): - return value - - try: - value = int(value) - return value - except (TypeError, ValueError): - pass - try: - value = float(value) - return value - except (TypeError, ValueError) as err: - raise vol.Invalid(f"invalid number {value}") from err - - -def control_scan_interval(config: dict) -> dict: - """Control scan_interval.""" - for hub in config: - minimum_scan_interval = DEFAULT_SCAN_INTERVAL - for component, conf_key in PLATFORMS: - if conf_key not in hub: - continue - - for entry in hub[conf_key]: - scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval < MINIMUM_SCAN_INTERVAL: - if scan_interval == 0: - continue - _LOGGER.warning( - "%s %s scan_interval(%d) is adjusted to minimum(%d)", - component, - entry.get(CONF_NAME), - scan_interval, - MINIMUM_SCAN_INTERVAL, - ) - scan_interval = MINIMUM_SCAN_INTERVAL - entry[CONF_SCAN_INTERVAL] = scan_interval - minimum_scan_interval = min(scan_interval, minimum_scan_interval) - if CONF_TIMEOUT in hub and hub[CONF_TIMEOUT] > minimum_scan_interval - 1: - _LOGGER.warning( - "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", - hub.get(CONF_NAME, ""), - hub[CONF_TIMEOUT], - minimum_scan_interval - 1, - ) - hub[CONF_TIMEOUT] = minimum_scan_interval - 1 - return config - - BASE_COMPONENT_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, @@ -311,7 +259,7 @@ SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ] ), vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_OFFSET, default=0): number, + vol.Optional(CONF_OFFSET, default=0): number_validator, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] @@ -320,7 +268,7 @@ SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] ), - vol.Optional(CONF_SCALE, default=1): number, + vol.Optional(CONF_SCALE, default=1): number_validator, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } @@ -380,7 +328,7 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.ensure_list, - control_scan_interval, + scan_interval_validator, [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index cd0c4524c74..0f376609de5 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -1,10 +1,19 @@ """Validate Modbus configuration.""" +from __future__ import annotations + import logging import struct +from typing import Any -from voluptuous import Invalid +import voluptuous as vol -from homeassistant.const import CONF_COUNT, CONF_NAME, CONF_STRUCTURE +from homeassistant.const import ( + CONF_COUNT, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_STRUCTURE, + CONF_TIMEOUT, +) from .const import ( CONF_DATA_TYPE, @@ -15,7 +24,10 @@ from .const import ( CONF_SWAP_WORD, DATA_TYPE_CUSTOM, DATA_TYPE_STRING, + DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCT_FORMAT, + MINIMUM_SCAN_INTERVAL, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) @@ -32,14 +44,14 @@ def sensor_schema_validator(config): f">{DEFAULT_STRUCT_FORMAT[config[CONF_DATA_TYPE]][config[CONF_COUNT]]}" ) except KeyError: - raise Invalid( + raise vol.Invalid( f"Unable to detect data type for {config[CONF_NAME]} sensor, try a custom type" ) from KeyError else: structure = config.get(CONF_STRUCTURE) if not structure: - raise Invalid( + raise vol.Invalid( f"Error in sensor {config[CONF_NAME]}. The `{CONF_STRUCTURE}` field can not be empty " f"if the parameter `{CONF_DATA_TYPE}` is set to the `{DATA_TYPE_CUSTOM}`" ) @@ -47,13 +59,13 @@ def sensor_schema_validator(config): try: size = struct.calcsize(structure) except struct.error as err: - raise Invalid( + raise vol.Invalid( f"Error in sensor {config[CONF_NAME]} structure: {str(err)}" ) from err bytecount = config[CONF_COUNT] * 2 if bytecount != size: - raise Invalid( + raise vol.Invalid( f"Structure request {size} bytes, " f"but {config[CONF_COUNT]} registers have a size of {bytecount} bytes" ) @@ -73,7 +85,7 @@ def sensor_schema_validator(config): else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD regs_needed = 2 if config[CONF_COUNT] < regs_needed or (config[CONF_COUNT] % regs_needed) != 0: - raise Invalid( + raise vol.Invalid( f"Error in sensor {config[CONF_NAME]} swap({swap_type}) " f"not possible due to the registers " f"count: {config[CONF_COUNT]}, needed: {regs_needed}" @@ -84,3 +96,56 @@ def sensor_schema_validator(config): CONF_STRUCTURE: structure, CONF_SWAP: swap_type, } + + +def number_validator(value: Any) -> int | float: + """Coerce a value to number without losing precision.""" + if isinstance(value, int): + return value + if isinstance(value, float): + return value + + try: + value = int(value) + return value + except (TypeError, ValueError): + pass + try: + value = float(value) + return value + except (TypeError, ValueError) as err: + raise vol.Invalid(f"invalid number {value}") from err + + +def scan_interval_validator(config: dict) -> dict: + """Control scan_interval.""" + for hub in config: + minimum_scan_interval = DEFAULT_SCAN_INTERVAL + for component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + + for entry in hub[conf_key]: + scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + if scan_interval < MINIMUM_SCAN_INTERVAL: + if scan_interval == 0: + continue + _LOGGER.warning( + "%s %s scan_interval(%d) is adjusted to minimum(%d)", + component, + entry.get(CONF_NAME), + scan_interval, + MINIMUM_SCAN_INTERVAL, + ) + scan_interval = MINIMUM_SCAN_INTERVAL + entry[CONF_SCAN_INTERVAL] = scan_interval + minimum_scan_interval = min(scan_interval, minimum_scan_interval) + if CONF_TIMEOUT in hub and hub[CONF_TIMEOUT] > minimum_scan_interval - 1: + _LOGGER.warning( + "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", + hub.get(CONF_NAME, ""), + hub[CONF_TIMEOUT], + minimum_scan_interval - 1, + ) + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + return config diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0819e5a3e89..8e45ee06976 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -19,7 +19,6 @@ import pytest import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.modbus import number from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, @@ -44,6 +43,7 @@ from homeassistant.components.modbus.const import ( SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) +from homeassistant.components.modbus.validators import number_validator from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ADDRESS, @@ -85,13 +85,13 @@ async def test_number_validator(): ("-15", int), ("-15.1", float), ]: - assert isinstance(number(value), value_type) + assert isinstance(number_validator(value), value_type) try: - number("x15.1") + number_validator("x15.1") except (vol.Invalid): return - pytest.fail("Number not throwing exception") + pytest.fail("Number_validator not throwing exception") @pytest.mark.parametrize( From 81097dbe40b2fcf996cc5b3d415c5074e1434e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 28 May 2021 12:02:35 +0200 Subject: [PATCH 036/750] Use get with default for consider home (#51194) --- homeassistant/components/fritz/common.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ad906e8956b..ec7e402f760 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -16,7 +16,10 @@ from fritzconnection.core.exceptions import ( from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus -from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -143,7 +146,9 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) - consider_home = self._options[CONF_CONSIDER_HOME] + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) new_device = False for known_host in self._update_info(): From e45196f9c961017ce0ecc70cbfaa1736869a1ed9 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 May 2021 12:06:46 +0200 Subject: [PATCH 037/750] Remove "old" config from modbus binary_sensor (#51117) --- .../components/modbus/binary_sensor.py | 75 ++----------------- tests/components/modbus/test_binary_sensor.py | 10 +-- 2 files changed, 10 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index c27fde6d946..bc586e2f24d 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -3,70 +3,19 @@ from __future__ import annotations import logging -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, - BinarySensorEntity, -) -from homeassistant.const import ( - CONF_ADDRESS, - CONF_BINARY_SENSORS, - CONF_DEVICE_CLASS, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_SLAVE, - STATE_ON, -) +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_platform import BasePlatform -from .const import ( - CALL_TYPE_COIL, - CALL_TYPE_DISCRETE, - CONF_COILS, - CONF_HUB, - CONF_INPUT_TYPE, - CONF_INPUTS, - DEFAULT_HUB, - DEFAULT_SCAN_INTERVAL, - MODBUS_DOMAIN, -) +from .const import MODBUS_DOMAIN PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_COILS, CONF_INPUTS), - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_INPUTS): [ - vol.All( - cv.deprecated(CALL_TYPE_COIL, CONF_ADDRESS), - vol.Schema( - { - vol.Required(CONF_ADDRESS): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int, - vol.Optional( - CONF_INPUT_TYPE, default=CALL_TYPE_COIL - ): vol.In([CALL_TYPE_COIL, CALL_TYPE_DISCRETE]), - } - ), - ) - ] - } - ), -) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -76,23 +25,11 @@ async def async_setup_platform( """Set up the Modbus binary sensors.""" sensors = [] - #  check for old config: - if discovery_info is None: - _LOGGER.warning( - "Binary_sensor configuration is deprecated, will be removed in a future release" - ) - discovery_info = { - CONF_NAME: "no name", - CONF_BINARY_SENSORS: config[CONF_INPUTS], - } + if discovery_info is None: # pragma: no cover + return for entry in discovery_info[CONF_BINARY_SENSORS]: - if CONF_HUB in entry: - hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] - else: - hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] - if CONF_SCAN_INTERVAL not in entry: - entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL + hub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append(ModbusBinarySensor(hub, entry)) async_add_entities(sensors) diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 5089d0271dd..ebf487b129d 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -6,7 +6,6 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CONF_INPUT_TYPE, - CONF_INPUTS, ) from homeassistant.const import ( CONF_ADDRESS, @@ -25,7 +24,6 @@ from .conftest import ReadResult, base_config_test, base_test, prepare_service_u from tests.common import mock_restore_cache -@pytest.mark.parametrize("do_discovery", [False, True]) @pytest.mark.parametrize( "do_options", [ @@ -37,7 +35,7 @@ from tests.common import mock_restore_cache }, ], ) -async def test_config_binary_sensor(hass, do_discovery, do_options): +async def test_config_binary_sensor(hass, do_options): """Run test for binary sensor.""" sensor_name = "test_sensor" config_sensor = { @@ -51,8 +49,8 @@ async def test_config_binary_sensor(hass, do_discovery, do_options): sensor_name, SENSOR_DOMAIN, CONF_BINARY_SENSORS, - CONF_INPUTS, - method_discovery=do_discovery, + None, + method_discovery=True, ) @@ -95,7 +93,7 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): sensor_name, SENSOR_DOMAIN, CONF_BINARY_SENSORS, - CONF_INPUTS, + None, regs, expected, method_discovery=True, From 39e62f9c90c8a7062c67d8af1ee32f75610f435a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 05:07:58 -0500 Subject: [PATCH 038/750] Improve Sonos polling (#51170) * Improve Sonos polling Warn user if polling is being used Provide callback IP:port to help user fix networking Fix radio handling when polling (no event payload) Clarify dispatch target to reflect polling action * Lint * Revert method removal --- .../components/sonos/binary_sensor.py | 3 +-- homeassistant/components/sonos/const.py | 2 +- homeassistant/components/sonos/entity.py | 18 +++++++++++++++--- homeassistant/components/sonos/media_player.py | 7 +++---- homeassistant/components/sonos/sensor.py | 3 +-- homeassistant/components/sonos/speaker.py | 17 +++++++++++++---- homeassistant/components/sonos/switch.py | 2 +- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 21e0c077136..8583132521c 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations -import datetime import logging from typing import Any @@ -50,7 +49,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Return the entity's device class.""" return DEVICE_CLASS_BATTERY_CHARGING - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index c32f981e345..0a70844e6b5 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -136,7 +136,7 @@ SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" -SONOS_ENTITY_UPDATE = "sonos_entity_update" +SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_ALARM_UPDATE = "sonos_alarm_update" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 8632357d618..8c47c69b2d7 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -1,6 +1,7 @@ """Entity representing a Sonos player.""" from __future__ import annotations +import datetime import logging from pysonos.core import SoCo @@ -15,8 +16,8 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_HOUSEHOLD_UPDATED, + SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, ) from .speaker import SonosSpeaker @@ -38,8 +39,8 @@ class SonosEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", - self.async_update, # pylint: disable=no-member + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + self.async_poll, ) ) self.async_on_remove( @@ -60,6 +61,17 @@ class SonosEntity(Entity): self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain ) + async def async_poll(self, now: datetime.datetime) -> None: + """Poll the entity if subscriptions fail.""" + if self.speaker.is_first_poll: + _LOGGER.warning( + "%s cannot reach [%s], falling back to polling, functionality may be limited", + self.speaker.zone_name, + self.speaker.subscription_address, + ) + self.speaker.is_first_poll = False + await self.async_update() # pylint: disable=no-member + @property def soco(self) -> SoCo: """Return the speaker SoCo instance.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 0200ae11aa8..133fe9efe8f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -293,13 +293,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_PLAYING return STATE_IDLE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Retrieve latest state.""" - await self.hass.async_add_executor_job(self._update, now) + await self.hass.async_add_executor_job(self._update) - def _update(self, now: datetime.datetime | None = None) -> None: + def _update(self) -> None: """Retrieve latest state.""" - _LOGGER.debug("Polling speaker %s", self.speaker.zone_name) try: self.speaker.update_groups() self.speaker.update_volume() diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index d9ff19af581..9e5277819a7 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -import datetime import logging from homeassistant.components.sensor import SensorEntity @@ -50,7 +49,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): """Get the unit of measurement.""" return PERCENTAGE - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index bb6cb306426..81c95f6e33f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -44,8 +44,8 @@ from .const import ( SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, SONOS_ENTITY_CREATED, - SONOS_ENTITY_UPDATE, SONOS_GROUP_UPDATE, + SONOS_POLL_UPDATE, SONOS_SEEN, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, @@ -138,6 +138,7 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + self.is_first_poll: bool = True self._is_ready: bool = False self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None @@ -322,7 +323,7 @@ class SonosSpeaker: partial( async_dispatcher_send, self.hass, - f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", + f"{SONOS_POLL_UPDATE}-{self.soco.uid}", ), SCAN_INTERVAL, ) @@ -418,7 +419,7 @@ class SonosSpeaker: ): async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) + async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" @@ -875,7 +876,7 @@ class SonosSpeaker: if not self.media.artist: try: self.media.artist = variables["current_track_meta_data"].creator - except (KeyError, AttributeError): + except (TypeError, KeyError, AttributeError): pass # Radios without tagging can have part of the radio URI as title. @@ -948,3 +949,11 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 879d5fa0a99..83449c846c6 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -112,7 +112,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return False - async def async_update(self, now: datetime.datetime | None = None) -> None: + async def async_update(self) -> None: """Poll the device for the current state.""" if await self.async_check_if_available(): await self.hass.async_add_executor_job(self.update_alarm) From 17b2678aee8c575acc5f301bea2ae8b8d9513caa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 May 2021 12:32:31 +0200 Subject: [PATCH 039/750] Define media_player entity attributes as class variables (#51192) --- .../components/dunehd/media_player.py | 4 +- homeassistant/components/heos/media_player.py | 3 +- .../components/media_player/__init__.py | 160 +++++++++++------- .../components/spotify/media_player.py | 54 ++---- 4 files changed, 115 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 17e9b6d9a37..482b92f768e 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN @@ -100,7 +100,7 @@ class DuneHDPlayerEntity(MediaPlayerEntity): return True @property - def state(self) -> StateType: + def state(self) -> str | None: """Return player state.""" state = STATE_OFF if "playback_position" in self._state: diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 63b592f1359..46a751983e9 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,7 +1,6 @@ """Denon HEOS Media Player.""" from __future__ import annotations -from collections.abc import Sequence from functools import reduce, wraps import logging from operator import ior @@ -362,7 +361,7 @@ class HeosMediaPlayer(MediaPlayerEntity): return self._source_manager.get_current_source(self._player.now_playing_media) @property - def source_list(self) -> Sequence[str]: + def source_list(self) -> list[str]: """List of available input sources.""" return self._source_manager.source_list diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 6fca2a4c3d5..0b4a5157c72 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -5,7 +5,7 @@ import asyncio import base64 import collections from contextlib import suppress -from datetime import timedelta +import datetime as dt import functools as ft import hashlib import logging @@ -57,6 +57,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + datetime, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -137,7 +138,7 @@ CACHE_URL = "url" CACHE_CONTENT = "content" ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16} -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = dt.timedelta(seconds=10) DEVICE_CLASS_TV = "tv" DEVICE_CLASS_SPEAKER = "speaker" @@ -371,11 +372,43 @@ class MediaPlayerEntity(Entity): _access_token: str | None = None + _attr_app_id: str | None = None + _attr_app_name: str | None = None + _attr_group_members: list[str] | None = None + _attr_is_volume_muted: bool | None = None + _attr_media_album_artist: str | None = None + _attr_media_album_name: str | None = None + _attr_media_artist: str | None = None + _attr_media_channel: str | None = None + _attr_media_content_id: str | None = None + _attr_media_content_type: str | None = None + _attr_media_duration: int | None = None + _attr_media_episode: str | None = None + _attr_media_image_hash: str | None + _attr_media_image_remotely_accessible: bool = False + _attr_media_image_url: str | None = None + _attr_media_playlist: str | None = None + _attr_media_position_updated_at: dt.datetime | None = None + _attr_media_position: int | None = None + _attr_media_season: str | None = None + _attr_media_series_title: str | None = None + _attr_media_title: str | None = None + _attr_media_track: int | None = None + _attr_repeat: str | None = None + _attr_shuffle: bool | None = None + _attr_sound_mode_list: list[str] | None = None + _attr_sound_mode: str | None = None + _attr_source_list: list[str] | None = None + _attr_source: str | None = None + _attr_state: str | None = None + _attr_supported_features: int = 0 + _attr_volume_level: float | None = None + # Implement these for your media player @property - def state(self): + def state(self) -> str | None: """State of the player.""" - return None + return self._attr_state @property def access_token(self) -> str: @@ -385,56 +418,59 @@ class MediaPlayerEntity(Entity): return self._access_token @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - return None + return self._attr_volume_level @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Boolean if volume is currently muted.""" - return None + return self._attr_is_volume_muted @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content ID of current playing media.""" - return None + return self._attr_media_content_id @property - def media_content_type(self): + def media_content_type(self) -> str | None: """Content type of current playing media.""" - return None + return self._attr_media_content_type @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return None + return self._attr_media_duration @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" - return None + return self._attr_media_position @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> dt.datetime | None: """When was the position of the current playing media valid. Returns value from homeassistant.util.dt.utcnow(). """ - return None + return self._attr_media_position_updated_at @property - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" - return None + return self._attr_media_image_url @property def media_image_remotely_accessible(self) -> bool: """If the image url is remotely accessible.""" - return False + return self._attr_media_image_remotely_accessible @property - def media_image_hash(self): + def media_image_hash(self) -> str | None: """Hash value for media image.""" + if hasattr(self, "_attr_media_image_hash"): + return self._attr_media_image_hash + url = self.media_image_url if url is not None: return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16] @@ -463,104 +499,104 @@ class MediaPlayerEntity(Entity): return None, None @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" - return None + return self._attr_media_title @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" - return None + return self._attr_media_artist @property - def media_album_name(self): + def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" - return None + return self._attr_media_album_name @property - def media_album_artist(self): + def media_album_artist(self) -> str | None: """Album artist of current playing media, music track only.""" - return None + return self._attr_media_album_artist @property - def media_track(self): + def media_track(self) -> int | None: """Track number of current playing media, music track only.""" - return None + return self._attr_media_track @property - def media_series_title(self): + def media_series_title(self) -> str | None: """Title of series of current playing media, TV show only.""" - return None + return self._attr_media_series_title @property - def media_season(self): + def media_season(self) -> str | None: """Season of current playing media, TV show only.""" - return None + return self._attr_media_season @property - def media_episode(self): + def media_episode(self) -> str | None: """Episode of current playing media, TV show only.""" - return None + return self._attr_media_episode @property - def media_channel(self): + def media_channel(self) -> str | None: """Channel currently playing.""" - return None + return self._attr_media_channel @property - def media_playlist(self): + def media_playlist(self) -> str | None: """Title of Playlist currently playing.""" - return None + return self._attr_media_playlist @property - def app_id(self): + def app_id(self) -> str | None: """ID of the current running app.""" - return None + return self._attr_app_id @property - def app_name(self): + def app_name(self) -> str | None: """Name of the current running app.""" - return None + return self._attr_app_name @property - def source(self): + def source(self) -> str | None: """Name of the current input source.""" - return None + return self._attr_source @property - def source_list(self): + def source_list(self) -> list[str] | None: """List of available input sources.""" - return None + return self._attr_source_list @property - def sound_mode(self): + def sound_mode(self) -> str | None: """Name of the current sound mode.""" - return None + return self._attr_sound_mode @property - def sound_mode_list(self): + def sound_mode_list(self) -> list[str] | None: """List of available sound modes.""" - return None + return self._attr_sound_mode_list @property - def shuffle(self): + def shuffle(self) -> bool | None: """Boolean if shuffle is enabled.""" - return None + return self._attr_shuffle @property - def repeat(self): + def repeat(self) -> str | None: """Return current repeat mode.""" - return None + return self._attr_repeat @property - def group_members(self): + def group_members(self) -> list[str] | None: """List of members which are currently grouped together.""" - return None + return self._attr_group_members @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" - return 0 + return self._attr_supported_features def turn_on(self): """Turn the media player on.""" diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e9ae2367273..1c92e2ce51a 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -67,8 +67,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -ICON = "mdi:spotify" - SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_SPOTIFY = ( @@ -211,12 +209,12 @@ def spotify_exception_handler(func): def wrapper(self, *args, **kwargs): try: result = func(self, *args, **kwargs) - self.player_available = True + self._attr_available = True return result except requests.RequestException: - self.player_available = False + self._attr_available = False except SpotifyException as exc: - self.player_available = False + self._attr_available = False if exc.reason == "NO_ACTIVE_DEVICE": raise HomeAssistantError("No active playback device found") from None @@ -226,6 +224,10 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" + _attr_icon = "mdi:spotify" + _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_image_remotely_accessible = False + def __init__( self, session: OAuth2Session, @@ -247,40 +249,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._currently_playing: dict | None = {} self._devices: list[dict] | None = [] self._playlist: dict | None = None - self._spotify: Spotify = None - self.player_available = False - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - return ICON - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.player_available - - @property - def unique_id(self) -> str: - """Return the unique ID.""" - return self._id + self._attr_name = self._name + self._attr_unique_id = user_id @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" + model = "Spotify Free" if self._me is not None: - model = self._me["product"] + product = self._me["product"] + model = f"Spotify {product}" return { "identifiers": {(DOMAIN, self._id)}, "manufacturer": "Spotify AB", - "model": f"Spotify {model}".rstrip(), + "model": model, "name": self._name, } @@ -304,11 +288,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): item = self._currently_playing.get("item") or {} return item.get("uri") - @property - def media_content_type(self) -> str | None: - """Return the media type.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" @@ -340,11 +319,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return None return fetch_image_url(self._currently_playing["item"]["album"]) - @property - def media_image_remotely_accessible(self) -> bool: - """If the image url is remotely accessible.""" - return False - @property def media_title(self) -> str | None: """Return the media title.""" @@ -357,7 +331,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): if self._currently_playing.get("item") is None: return None return ", ".join( - [artist["name"] for artist in self._currently_playing["item"]["artists"]] + artist["name"] for artist in self._currently_playing["item"]["artists"] ) @property From b339d7310940fc1b7db6a947211e725d37e4f4f5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 13:16:52 +0200 Subject: [PATCH 040/750] Weight sensor average statistics by state durations (#51150) * Weight sensor average statistics by state durations * Fix test --- homeassistant/components/sensor/recorder.py | 45 +++++++++++++++++--- tests/components/recorder/test_statistics.py | 2 +- tests/components/sensor/test_recorder.py | 24 +++++------ 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index a75aa6298bc..fb6c8d2fba3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,7 +3,6 @@ from __future__ import annotations import datetime import itertools -from statistics import fmean from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( @@ -16,7 +15,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, ) from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util from . import DOMAIN @@ -53,6 +52,44 @@ def _is_number(s: str) -> bool: # pylint: disable=invalid-name return s.replace(".", "", 1).isdigit() +def _time_weighted_average( + fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime +) -> float: + """Calculate a time weighted average. + + The average is calculated by, weighting the states by duration in seconds between + state changes. + Note: there's no interpolation of values between state changes. + """ + old_fstate: float | None = None + old_start_time: datetime.datetime | None = None + accumulated = 0.0 + + for fstate, state in fstates: + # The recorder will give us the last known state, which may be well + # before the requested start time for the statistics + start_time = start if state.last_updated < start else state.last_updated + if old_start_time is None: + # Adjust start time, if there was no last known state + start = start_time + else: + duration = start_time - old_start_time + # Accumulate the value, weighted by duration until next state change + assert old_fstate is not None + accumulated += old_fstate * duration.total_seconds() + + old_fstate = fstate + old_start_time = start_time + + if old_fstate is not None: + # Accumulate the value, weighted by duration until end of the period + assert old_start_time is not None + duration = end - old_start_time + accumulated += old_fstate * duration.total_seconds() + + return accumulated / (end - start).total_seconds() + + def compile_statistics( hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime ) -> dict: @@ -91,10 +128,8 @@ def compile_statistics( if "min" in wanted_statistics: result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1)) - # Note: The average calculation will be incorrect for unevenly spaced readings, - # this needs to be improved by weighting with time between measurements if "mean" in wanted_statistics: - result[entity_id]["mean"] = fmean(*itertools.islice(zip(*fstates), 1)) + result[entity_id]["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: last_reset = old_last_reset = None diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 74be1075626..cffb67937fe 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -30,7 +30,7 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 14.915254237288135, "min": 10.0, "max": 20.0, "last_reset": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 5d86ac520a5..37cc7387f25 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -31,9 +31,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 15.0, + "mean": 16.440677966101696, "min": 10.0, - "max": 20.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -243,9 +243,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 20.0, - "min": 20.0, - "max": 20.0, + "mean": 30.0, + "min": 30.0, + "max": 30.0, "last_reset": None, "state": None, "sum": None, @@ -271,7 +271,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 17.5, + "mean": 21.1864406779661, "min": 10.0, "max": 25.0, "last_reset": None, @@ -318,9 +318,9 @@ def record_states(hass): zero = dt_util.utcnow() one = zero + timedelta(minutes=1) - two = one + timedelta(minutes=15) - three = two + timedelta(minutes=30) - four = three + timedelta(minutes=15) + two = one + timedelta(minutes=10) + three = two + timedelta(minutes=40) + four = three + timedelta(minutes=10) states = {mp: [], sns1: [], sns2: [], sns3: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): @@ -340,9 +340,9 @@ def record_states(hass): states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) return zero, four, states From ab2ea35b7dff55f61957cfab8a821489ab7f4973 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 May 2021 13:22:58 +0200 Subject: [PATCH 041/750] Update base image to 2021.05.0 (#51198) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index de5f895af2a..c3e5d83dc78 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.3", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.3", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0" }, "labels": { "io.hass.type": "core", From 00507539c1b5f34c6ac1b3a30acdace9d29788c0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 May 2021 13:23:44 +0200 Subject: [PATCH 042/750] Change Cover to use address/input_type (#51154) * Change Cover to use address/input_type. * Flake. --- homeassistant/components/modbus/__init__.py | 39 +++++++------ homeassistant/components/modbus/cover.py | 64 ++++++--------------- tests/components/modbus/test_cover.py | 25 +++++--- 3 files changed, 55 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d4b70796c1a..f60c765d80c 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -68,7 +68,6 @@ from .const import ( CONF_MIN_TEMP, CONF_PARITY, CONF_PRECISION, - CONF_REGISTER, CONF_REVERSE_ORDER, CONF_SCALE, CONF_STATE_CLOSED, @@ -145,24 +144,26 @@ CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( } ) -COVERS_SCHEMA = vol.All( - cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER), - BASE_COMPONENT_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int, - vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int, - vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int, - vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int, - vol.Optional(CONF_STATUS_REGISTER): cv.positive_int, - vol.Optional( - CONF_STATUS_REGISTER_TYPE, - default=CALL_TYPE_REGISTER_HOLDING, - ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), - vol.Exclusive(CALL_TYPE_COIL, CONF_INPUT_TYPE): cv.positive_int, - vol.Exclusive(CONF_REGISTER, CONF_INPUT_TYPE): cv.positive_int, - } - ), +COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING,): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int, + vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int, + vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int, + vol.Optional(CONF_STATE_OPENING, default=2): cv.positive_int, + vol.Optional(CONF_STATUS_REGISTER): cv.positive_int, + vol.Optional( + CONF_STATUS_REGISTER_TYPE, + default=CALL_TYPE_REGISTER_HOLDING, + ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), + } ) SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 2317bc5401f..88c8fd77ae8 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity from homeassistant.const import ( - CONF_ADDRESS, CONF_COVERS, CONF_NAME, STATE_CLOSED, @@ -23,11 +22,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, - CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_COIL, CALL_TYPE_WRITE_REGISTER, - CONF_INPUT_TYPE, - CONF_REGISTER, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OPEN, @@ -69,11 +65,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): config: dict[str, Any], ) -> None: """Initialize the modbus cover.""" - config[CONF_ADDRESS] = "0" - config[CONF_INPUT_TYPE] = "" super().__init__(hub, config) - self._coil = config.get(CALL_TYPE_COIL) - self._register = config.get(CONF_REGISTER) self._state_closed = config[CONF_STATE_CLOSED] self._state_closing = config[CONF_STATE_CLOSING] self._state_open = config[CONF_STATE_OPEN] @@ -84,22 +76,23 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): # If we read cover status from coil, and not from optional status register, # we interpret boolean value False as closed cover, and value True as open cover. # Intermediate states are not supported in such a setup. - if self._coil is not None: + if self._input_type == CALL_TYPE_COIL: self._write_type = CALL_TYPE_WRITE_COIL + self._write_address = self._address if self._status_register is None: self._state_closed = False self._state_open = True self._state_closing = None self._state_opening = None - - # If we read cover status from the main register (i.e., an optional - # status register is not specified), we need to make sure the register_type - # is set to "holding". - if self._register is not None: + else: + # If we read cover status from the main register (i.e., an optional + # status register is not specified), we need to make sure the register_type + # is set to "holding". self._write_type = CALL_TYPE_WRITE_REGISTER - if self._status_register is None: - self._status_register = self._register - self._status_register_type = CALL_TYPE_REGISTER_HOLDING + self._write_address = self._address + if self._status_register: + self._address = self._status_register + self._input_type = self._status_register_type async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -139,7 +132,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" result = await self._hub.async_pymodbus_call( - self._slave, self._register, self._state_open, self._write_type + self._slave, self._write_address, self._state_open, self._write_type ) self._available = result is not None await self.async_update() @@ -147,7 +140,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" result = await self._hub.async_pymodbus_call( - self._slave, self._register, self._state_closed, self._write_type + self._slave, self._write_address, self._state_closed, self._write_type ) self._available = result is not None await self.async_update() @@ -156,35 +149,16 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): """Update the state of the cover.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - if self._coil is not None and self._status_register is None: - self._value = await self._async_read_coil() - else: - self._value = await self._async_read_status_register() - - self.async_write_ha_state() - - async def _async_read_status_register(self) -> int | None: - """Read status register using the Modbus hub slave.""" result = await self._hub.async_pymodbus_call( - self._slave, self._status_register, 1, self._status_register_type + self._slave, self._address, 1, self._input_type ) if result is None: self._available = False + self.async_write_ha_state() return None - - value = int(result.registers[0]) self._available = True - - return value - - async def _async_read_coil(self) -> bool | None: - """Read coil using the Modbus hub slave.""" - result = await self._hub.async_pymodbus_call( - self._slave, self._coil, 1, CALL_TYPE_COIL - ) - if result is None: - self._available = False - return None - - value = bool(result.bits[0] & 1) - return value + if self._input_type == CALL_TYPE_COIL: + self._value = bool(result.bits[0] & 1) + else: + self._value = int(result.registers[0]) + self.async_write_ha_state() diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 4073e46a86e..98c4e84699f 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -7,7 +7,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, - CONF_REGISTER, + CONF_INPUT_TYPE, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OPEN, @@ -16,6 +16,7 @@ from homeassistant.components.modbus.const import ( CONF_STATUS_REGISTER_TYPE, ) from homeassistant.const import ( + CONF_ADDRESS, CONF_COVERS, CONF_NAME, CONF_SCAN_INTERVAL, @@ -43,13 +44,14 @@ from tests.common import mock_restore_cache }, ], ) -@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) async def test_config_cover(hass, do_options, read_type): """Run test for cover.""" device_name = "test_cover" device_config = { CONF_NAME: device_name, - read_type: 1234, + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: read_type, **do_options, } await base_config_test( @@ -95,7 +97,8 @@ async def test_coil_cover(hass, regs, expected): hass, { CONF_NAME: cover_name, - CALL_TYPE_COIL: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, cover_name, @@ -142,7 +145,7 @@ async def test_register_cover(hass, regs, expected): hass, { CONF_NAME: cover_name, - CONF_REGISTER: 1234, + CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, cover_name, @@ -165,7 +168,7 @@ async def test_service_cover_update(hass, mock_pymodbus): CONF_COVERS: [ { CONF_NAME: "test", - CONF_REGISTER: 1234, + CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, } ] @@ -196,7 +199,8 @@ async def test_restore_state_cover(hass, state): cover_name = "test" config = { CONF_NAME: cover_name, - CALL_TYPE_COIL: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, CONF_STATE_OPEN: 1, CONF_STATE_CLOSED: 0, CONF_STATE_OPENING: 2, @@ -229,12 +233,13 @@ async def test_service_cover_move(hass, mock_pymodbus): CONF_COVERS: [ { CONF_NAME: "test", - CONF_REGISTER: 1234, + CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { CONF_NAME: "test2", - CALL_TYPE_COIL: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, }, ] } @@ -254,10 +259,12 @@ async def test_service_cover_move(hass, mock_pymodbus): ) assert hass.states.get(entity_id).state == STATE_CLOSED + mock_pymodbus.reset() mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": entity_id}, blocking=True ) + assert mock_pymodbus.read_holding_registers.called assert hass.states.get(entity_id).state == STATE_UNAVAILABLE mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") From b6cb123c4fb5bd36c318cef06efdc63e6d1077a3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 28 May 2021 13:32:26 +0200 Subject: [PATCH 043/750] Only run philips_js notify service while TV is turned on (#51196) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .../components/philips_js/__init__.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index ffa2109a6e5..b4e086ef391 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -116,8 +116,21 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + @property + def _notify_wanted(self): + """Return if the notify feature should be active. + + We only run it when TV is considered fully on. When powerstate is in standby, the TV + will go in low power states and seemingly break the http server in odd ways. + """ + return ( + self.api.on + and self.api.powerstate == "On" + and self.api.notify_change_supported + ) + async def _notify_task(self): - while self.api.on and self.api.notify_change_supported: + while self._notify_wanted: res = await self.api.notifyChange(130) if res: self.async_set_updated_data(None) @@ -133,11 +146,10 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @callback def _async_notify_schedule(self): - if ( - (self._notify_future is None or self._notify_future.done()) - and self.api.on - and self.api.notify_change_supported - ): + if self._notify_future and not self._notify_future.done(): + return + + if self._notify_wanted: self._notify_future = asyncio.create_task(self._notify_task()) @callback From ac922916c17c0f0a97f02213244cc45edc564853 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 28 May 2021 13:36:22 +0200 Subject: [PATCH 044/750] Fix Netatmo sensor initialization (#51195) --- homeassistant/components/netatmo/__init__.py | 7 +++++++ homeassistant/components/netatmo/data_handler.py | 10 ++++++---- .../components/netatmo/netatmo_entity_base.py | 4 ---- homeassistant/components/netatmo/sensor.py | 14 +++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index c401b46f981..8158c23742f 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -204,9 +204,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_register(DOMAIN, "register_webhook", register_webhook) hass.services.async_register(DOMAIN, "unregister_webhook", unregister_webhook) + entry.add_update_listener(async_config_entry_updated) + return True +async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated.""" + async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 11415417ee1..12376e5ac78 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -95,12 +95,14 @@ class NetatmoDataHandler: for data_class in islice(self._queue, 0, BATCH_SIZE): if data_class[NEXT_SCAN] > time(): continue - self.data_classes[data_class["name"]][NEXT_SCAN] = ( - time() + data_class["interval"] - ) if data_class_name := data_class["name"]: - await self.async_fetch_data(data_class_name) + self.data_classes[data_class_name][NEXT_SCAN] = ( + time() + data_class["interval"] + ) + + if self.data_classes[data_class_name]["subscriptions"]: + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 1fcd4a121d8..5d43a46e89b 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,16 +1,12 @@ """Base class for Netatmo entities.""" from __future__ import annotations -import logging - from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler -_LOGGER = logging.getLogger(__name__) - class NetatmoBase(Entity): """Netatmo entity base class.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index e56847386a3..eeaab52a21a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -21,7 +20,7 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( @@ -131,6 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + platform_not_ready = False async def find_entities(data_class_name): """Find all entities.""" @@ -184,7 +184,7 @@ async def async_setup_entry(hass, entry, async_add_entities): data_class = data_handler.data.get(data_class_name) if not data_class or not data_class.raw_data: - raise PlatformNotReady + platform_not_ready = True async_add_entities(await find_entities(data_class_name), True) @@ -241,14 +241,10 @@ async def async_setup_entry(hass, entry, async_add_entities): hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) - entry.add_update_listener(async_config_entry_updated) - await add_public_entities(False) - -async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle signals of config entry being updated.""" - async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + if platform_not_ready: + raise PlatformNotReady class NetatmoSensor(NetatmoBase, SensorEntity): From fe0771f7f1949fdb7973b15c711ae2f4213c8341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 28 May 2021 13:41:40 +0200 Subject: [PATCH 045/750] Add missing outdoor temperature unit for Tado (#51197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix outdoor temperature unit for Tado Signed-off-by: Álvaro Fernández Rojas * tado: simplify if conditions Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/tado/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 87d2170eb75..e1219b5620b 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -139,7 +139,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - if self.home_variable == "temperature": + if self.home_variable in ["temperature", "outdoor temperature"]: return TEMP_CELSIUS if self.home_variable == "solar percentage": return PERCENTAGE From 076544a1b5869f5aea63974409446a25d424e182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 28 May 2021 14:00:16 +0200 Subject: [PATCH 046/750] Revert "Bump config version to 2 for AVM Fritz Tools (#51176)" (#51193) --- homeassistant/components/fritz/__init__.py | 20 ------------------- homeassistant/components/fritz/config_flow.py | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index b310d44a0f9..35e924c807c 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -3,10 +3,6 @@ import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError -from homeassistant.components.device_tracker.const import ( - CONF_CONSIDER_HOME, - DEFAULT_CONSIDER_HOME, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -67,22 +63,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Migrate config entry.""" - _LOGGER.debug("Migrating from version %s", config_entry.version) - - if config_entry.version == 1: - config_entry.version = 2 - hass.config_entries.async_update_entry( - config_entry, - options={CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds()}, - ) - - _LOGGER.info("Migration to version %s successful", config_entry.version) - - return True - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FRITZ!Box Tools config entry.""" fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 9d3c9044765..4001dcadc71 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -38,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a FRITZ!Box Tools config flow.""" - VERSION = 2 + VERSION = 1 @staticmethod @callback From 0fbdce5ca690beafb18cb2f0d90cd41bf61e8626 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 28 May 2021 14:38:01 +0200 Subject: [PATCH 047/750] Update frontend to 20210528.0 (#51199) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 57380b53be8..9ee97c851e9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210526.0" + "home-assistant-frontend==20210528.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b61e0d24785..7e6be2f0016 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 5bcd60f10b1..cb71ab9f023 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ae205c1e23..9641d89e3a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210526.0 +home-assistant-frontend==20210528.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 187374c11edfc8ab87b7e99cb9b336cb571b01e1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 28 May 2021 15:29:11 +0200 Subject: [PATCH 048/750] Set Registry name parameter to Hashable type (#51203) --- homeassistant/util/decorator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index 83c63711c7d..d2943d39979 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -1,4 +1,5 @@ """Decorator utility functions.""" +from collections.abc import Hashable from typing import Callable, TypeVar CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name @@ -7,7 +8,7 @@ CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-na class Registry(dict): """Registry of items.""" - def register(self, name: str) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: Hashable) -> Callable[[CALLABLE_T], CALLABLE_T]: """Return decorator to register item with a specific name.""" def decorator(func: CALLABLE_T) -> CALLABLE_T: From b3d826f2e20d81f8fc94172e4d825d1c64b28805 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 May 2021 09:06:17 -0500 Subject: [PATCH 049/750] Fix samsungtv yaml import without configured name (#51204) --- .../components/samsungtv/config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index b45f6c5670b..46800e1653b 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -173,7 +173,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except socket.gaierror as err: raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err - self._name = user_input[CONF_NAME] + self._name = user_input.get(CONF_NAME, self._host) self._title = self._name async def async_step_user(self, user_input=None): diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 04dffd31801..5b85ecf7048 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -54,6 +54,9 @@ MOCK_IMPORT_DATA = { CONF_NAME: "fake", CONF_PORT: 55000, } +MOCK_IMPORT_DATA_WITHOUT_NAME = { + CONF_HOST: "fake_host", +} MOCK_IMPORT_WSDATA = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -509,6 +512,26 @@ async def test_import_legacy(hass: HomeAssistant): assert result["result"].unique_id is None +async def test_import_legacy_without_name(hass: HomeAssistant): + """Test importing from yaml without a name.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" with patch( From 99ee2bd0a3ec9d82f6add356b2f52787310e7494 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 28 May 2021 17:48:30 +0300 Subject: [PATCH 050/750] Update to pymelcloud 2.5.3 (#51043) Previous version of pymelcloud performs requests that are not permitted for guest users. Bypassing these requests results only in less detailed device info. --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 641a4df583e..4aff46a22b6 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.2"], + "requirements": ["pymelcloud==2.5.3"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index cb71ab9f023..e4fb32e1c6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1557,7 +1557,7 @@ pymazda==0.1.6 pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9641d89e3a3..d383468e840 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pymata-express==1.19 pymazda==0.1.6 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 From 88dce0ec8f026b668ce46921f67f87b073c3edd9 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 28 May 2021 08:54:19 -0700 Subject: [PATCH 051/750] Address late review of Mazda services (#51178) * Add services for Mazda integration * Address review comments * Follow-up review comments * Update dict access for send_poi service calls --- homeassistant/components/mazda/__init__.py | 18 +++++----- tests/components/mazda/test_init.py | 38 +++++++++++++--------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 469e28eb829..d704cfb7f44 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -69,16 +69,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle a service call.""" # Get device entry from device registry dev_reg = device_registry.async_get(hass) - device_id = service_call.data.get("device_id") + device_id = service_call.data["device_id"] device_entry = dev_reg.async_get(device_id) # Get vehicle VIN from device identifiers - mazda_identifiers = [ + mazda_identifiers = ( identifier for identifier in device_entry.identifiers if identifier[0] == DOMAIN - ] - vin_identifier = next(iter(mazda_identifiers)) + ) + vin_identifier = next(mazda_identifiers) vin = vin_identifier[1] # Get vehicle ID and API client from hass.data @@ -89,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if vehicle["vin"] == vin: vehicle_id = vehicle["id"] api_client = entry_data[DATA_CLIENT] + break if vehicle_id == 0 or api_client is None: raise HomeAssistantError("Vehicle ID not found") @@ -96,14 +97,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_method = getattr(api_client, service_call.service) try: if service_call.service == "send_poi": - latitude = service_call.data.get("latitude") - longitude = service_call.data.get("longitude") - poi_name = service_call.data.get("poi_name") + latitude = service_call.data["latitude"] + longitude = service_call.data["longitude"] + poi_name = service_call.data["poi_name"] await api_method(vehicle_id, latitude, longitude, poi_name) else: await api_method(vehicle_id) except Exception as ex: - _LOGGER.exception("Error occurred during Mazda service call: %s", ex) raise HomeAssistantError(ex) from ex def validate_mazda_device_id(device_id): @@ -119,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for identifier in device_entry.identifiers if identifier[0] == DOMAIN ] - if len(mazda_identifiers) < 1: + if not mazda_identifiers: raise vol.Invalid("Device ID is not a Mazda vehicle") return device_id diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 0280e8f34fa..0c47ae8f2e0 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -7,7 +7,7 @@ from pymazda import MazdaAuthenticationException, MazdaException import pytest import voluptuous as vol -from homeassistant.components.mazda.const import DOMAIN, SERVICES +from homeassistant.components.mazda.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_EMAIL, @@ -186,7 +186,23 @@ async def test_device_no_nickname(hass): assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" -async def test_services(hass): +@pytest.mark.parametrize( + "service, service_data, expected_args", + [ + ("start_charging", {}, [12345]), + ("start_engine", {}, [12345]), + ("stop_charging", {}, [12345]), + ("stop_engine", {}, [12345]), + ("turn_off_hazard_lights", {}, [12345]), + ("turn_on_hazard_lights", {}, [12345]), + ( + "send_poi", + {"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"}, + [12345, 1.2345, 2.3456, "Work"], + ), + ], +) +async def test_services(hass, service, service_data, expected_args): """Test service calls.""" client_mock = await init_integration(hass) @@ -196,21 +212,13 @@ async def test_services(hass): ) device_id = reg_device.id - for service in SERVICES: - service_data = {"device_id": device_id} - if service == "send_poi": - service_data["latitude"] = 1.2345 - service_data["longitude"] = 2.3456 - service_data["poi_name"] = "Work" + service_data["device_id"] = device_id - await hass.services.async_call(DOMAIN, service, service_data, blocking=True) - await hass.async_block_till_done() + await hass.services.async_call(DOMAIN, service, service_data, blocking=True) + await hass.async_block_till_done() - api_method = getattr(client_mock, service) - if service == "send_poi": - api_method.assert_called_once_with(12345, 1.2345, 2.3456, "Work") - else: - api_method.assert_called_once_with(12345) + api_method = getattr(client_mock, service) + api_method.assert_called_once_with(*expected_args) async def test_service_invalid_device_id(hass): From 538a189168b127aee7b1742bbe9ebd4debca71f4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 28 May 2021 17:57:14 +0200 Subject: [PATCH 052/750] Adjust modbus climate to use address/input_type (#51202) --- homeassistant/components/modbus/__init__.py | 13 +++++++------ homeassistant/components/modbus/climate.py | 12 +----------- tests/components/modbus/test_climate.py | 10 +++++----- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index f60c765d80c..8d00a1ba7c5 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -58,8 +58,6 @@ from .const import ( CONF_BYTESIZE, CONF_CLIMATES, CONF_CLOSE_COMM_ON_ERROR, - CONF_CURRENT_TEMP, - CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, CONF_DATA_TYPE, CONF_FANS, @@ -124,12 +122,15 @@ BASE_COMPONENT_SCHEMA = vol.Schema( CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { - vol.Required(CONF_CURRENT_TEMP): cv.positive_int, + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + ] + ), vol.Required(CONF_TARGET_TEMP): cv.positive_int, vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int, - vol.Optional( - CONF_CURRENT_TEMP_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING - ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 4e6a20b1700..15a14b7eca9 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -11,7 +11,6 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - CONF_ADDRESS, CONF_NAME, CONF_OFFSET, CONF_STRUCTURE, @@ -29,11 +28,8 @@ from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, - CONF_CURRENT_TEMP, - CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, CONF_DATA_TYPE, - CONF_INPUT_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_PRECISION, @@ -108,14 +104,8 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): config: dict[str, Any], ) -> None: """Initialize the modbus thermostat.""" - config[CONF_ADDRESS] = "0" - config[CONF_INPUT_TYPE] = "" super().__init__(hub, config) self._target_temperature_register = config[CONF_TARGET_TEMP] - self._current_temperature_register = config[CONF_CURRENT_TEMP] - self._current_temperature_register_type = config[ - CONF_CURRENT_TEMP_REGISTER_TYPE - ] self._target_temperature = None self._current_temperature = None self._data_type = config[CONF_DATA_TYPE] @@ -212,7 +202,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) self._current_temperature = await self._async_read_register( - self._current_temperature_register_type, self._current_temperature_register + self._input_type, self._address ) self.async_write_ha_state() diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index c73a73e47e8..50ba518bb36 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -5,12 +5,12 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import HVAC_MODE_AUTO from homeassistant.components.modbus.const import ( CONF_CLIMATES, - CONF_CURRENT_TEMP, CONF_DATA_COUNT, CONF_TARGET_TEMP, ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_ADDRESS, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, @@ -38,7 +38,7 @@ async def test_config_climate(hass, do_options): device_config = { CONF_NAME: device_name, CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, + CONF_ADDRESS: 117, CONF_SLAVE: 10, **do_options, } @@ -72,7 +72,7 @@ async def test_temperature_climate(hass, regs, expected): CONF_NAME: climate_name, CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, + CONF_ADDRESS: 117, CONF_DATA_COUNT: 2, }, climate_name, @@ -96,7 +96,7 @@ async def test_service_climate_update(hass, mock_pymodbus): { CONF_NAME: "test", CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, + CONF_ADDRESS: 117, CONF_SLAVE: 10, } ] @@ -123,7 +123,7 @@ async def test_restore_state_climate(hass): config_sensor = { CONF_NAME: climate_name, CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, + CONF_ADDRESS: 117, } mock_restore_cache( hass, From 39c94e8daa31d293505daac34eca09f72a897ded Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 28 May 2021 22:37:17 +0200 Subject: [PATCH 053/750] Fix flaky statistics tests (#51214) * Fix flaky statistics tests * Tweak --- tests/components/recorder/test_statistics.py | 8 ++- tests/components/sensor/test_recorder.py | 68 ++++++++++---------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index cffb67937fe..1ec0f2284b4 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -30,9 +32,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 14.915254237288135, - "min": 10.0, - "max": 20.0, + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), "last_reset": None, "state": None, "sum": None, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 37cc7387f25..47a950f9eaa 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3,6 +3,8 @@ from datetime import timedelta from unittest.mock import patch, sentinel +from pytest import approx + from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat @@ -31,9 +33,9 @@ def test_compile_hourly_statistics(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 16.440677966101696, - "min": 10.0, - "max": 30.0, + "mean": approx(16.440677966101696), + "min": approx(10.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -75,8 +77,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -85,8 +87,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -95,8 +97,8 @@ def test_compile_hourly_energy_statistics(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ] } @@ -135,8 +137,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 20.0, - "sum": 10.0, + "state": approx(20.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -145,8 +147,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 40.0, - "sum": 10.0, + "state": approx(40.0), + "sum": approx(10.0), }, { "statistic_id": "sensor.test1", @@ -155,8 +157,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 70.0, - "sum": 40.0, + "state": approx(70.0), + "sum": approx(40.0), }, ], "sensor.test2": [ @@ -167,8 +169,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 130.0, - "sum": 20.0, + "state": approx(130.0), + "sum": approx(20.0), }, { "statistic_id": "sensor.test2", @@ -177,8 +179,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 45.0, - "sum": -95.0, + "state": approx(45.0), + "sum": approx(-95.0), }, { "statistic_id": "sensor.test2", @@ -187,8 +189,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 75.0, - "sum": -65.0, + "state": approx(75.0), + "sum": approx(-65.0), }, ], "sensor.test3": [ @@ -199,8 +201,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": 5.0, - "sum": 5.0, + "state": approx(5.0), + "sum": approx(5.0), }, { "statistic_id": "sensor.test3", @@ -209,8 +211,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 50.0, - "sum": 30.0, + "state": approx(50.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test3", @@ -219,8 +221,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": 90.0, - "sum": 70.0, + "state": approx(90.0), + "sum": approx(70.0), }, ], } @@ -243,9 +245,9 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": 30.0, - "min": 30.0, - "max": 30.0, + "mean": approx(30.0), + "min": approx(30.0), + "max": approx(30.0), "last_reset": None, "state": None, "sum": None, @@ -271,9 +273,9 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(zero), - "mean": 21.1864406779661, - "min": 10.0, - "max": 25.0, + "mean": approx(21.1864406779661), + "min": approx(10.0), + "max": approx(25.0), "last_reset": None, "state": None, "sum": None, From 19c16e079f1d75e65f59380e9ae4cb43f92f054a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 28 May 2021 17:24:01 -0400 Subject: [PATCH 054/750] Add separate ozone sensor for climacell (#51182) --- homeassistant/components/climacell/const.py | 10 ++++++++++ tests/components/climacell/test_sensor.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 0352807138a..5b62d05a78c 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -128,6 +128,11 @@ CC_ATTR_POLLEN_GRASS = "grassIndex" CC_ATTR_FIRE_INDEX = "fireIndex" CC_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_ATTR_OZONE, + ATTR_NAME: "Ozone", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, { ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, ATTR_NAME: "Particulate Matter < 2.5 μm", @@ -262,6 +267,11 @@ CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" CC_V3_ATTR_FIRE_INDEX = "fire_index" CC_V3_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_V3_ATTR_OZONE, + ATTR_NAME: "Ozone", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, { ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25, ATTR_NAME: "Particulate Matter < 2.5 μm", diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index d06742ba209..653a989c4b7 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -26,6 +26,7 @@ from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) CC_SENSOR_ENTITY_ID = "sensor.climacell_{}" +O3 = "ozone" CO = "carbon_monoxide" NO2 = "nitrogen_dioxide" SO2 = "sulfur_dioxide" @@ -72,6 +73,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() for entity_name in ( + O3, CO, NO2, SO2, @@ -90,7 +92,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: ): _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 15 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 16 def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): @@ -107,6 +109,7 @@ async def test_v3_sensor( ) -> None: """Test v3 sensor data.""" await _setup(hass, API_V3_ENTRY_DATA) + check_sensor_state(hass, O3, "52.625") check_sensor_state(hass, CO, "0.875") check_sensor_state(hass, NO2, "14.1875") check_sensor_state(hass, SO2, "2") @@ -130,6 +133,7 @@ async def test_v4_sensor( ) -> None: """Test v4 sensor data.""" await _setup(hass, API_V4_ENTRY_DATA) + check_sensor_state(hass, O3, "46.53") check_sensor_state(hass, CO, "0.63") check_sensor_state(hass, NO2, "10.67") check_sensor_state(hass, SO2, "1.65") From 4b2831dddeef6fe67da5e5b40862fbf9c768b5ad Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 17:45:43 -0500 Subject: [PATCH 055/750] Improve Sonos alarm logging (#51212) --- homeassistant/components/sonos/switch.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 83449c846c6..2038002db2d 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -104,7 +104,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True - _LOGGER.debug("The alarm is removed from hass because it has been deleted") + _LOGGER.debug("%s has been deleted", self.entity_id) entity_registry = er.async_get(self.hass) if entity_registry.async_get(self.entity_id): @@ -119,7 +119,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): def update_alarm(self): """Update the state of the alarm.""" - _LOGGER.debug("Updating the state of the alarm") + _LOGGER.debug("Updating alarm: %s", self.entity_id) if self.speaker.soco.uid != self.alarm.zone.uid: self.speaker = self.hass.data[DATA_SONOS].discovered.get( self.alarm.zone.uid @@ -150,7 +150,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) if not entity_registry.async_get(self.entity_id).device_id == new_device.id: - _LOGGER.debug("The alarm is switching the sonos player") + _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) # pylint: disable=protected-access entity_registry._async_update_entity( self.entity_id, device_id=new_device.id @@ -199,10 +199,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_handle_switch_on_off(self, turn_on: bool) -> None: """Handle turn on/off of alarm switch.""" try: - _LOGGER.debug("Switching the state of the alarm") + _LOGGER.debug("Toggling the state of %s", self.entity_id) self.alarm.enabled = turn_on await self.hass.async_add_executor_job(self.alarm.save) except SoCoUPnPException as exc: - _LOGGER.warning( - "Home Assistant couldn't switch the alarm %s", exc, exc_info=True - ) + _LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True) From 8e87d638e1c4c03ed0a75d72211e5548c84fdf94 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 29 May 2021 00:21:56 +0000 Subject: [PATCH 056/750] [ci skip] Translation update --- homeassistant/components/motioneye/translations/no.json | 4 ++++ homeassistant/components/samsungtv/translations/ca.json | 8 ++++---- homeassistant/components/samsungtv/translations/nl.json | 4 ++-- homeassistant/components/sia/translations/ca.json | 2 +- homeassistant/components/sia/translations/no.json | 2 +- .../components/totalconnect/translations/ca.json | 2 +- .../components/totalconnect/translations/no.json | 5 +++-- homeassistant/components/tuya/translations/nl.json | 2 +- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json index 5b7f6538bb8..33dadffa94f 100644 --- a/homeassistant/components/motioneye/translations/no.json +++ b/homeassistant/components/motioneye/translations/no.json @@ -11,6 +11,10 @@ "unknown": "Uventet feil" }, "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til motionEye-tjenesten som tilbys av tillegget: {addon} ?", + "title": "motionEye via Home Assistant-tillegget" + }, "user": { "data": { "admin_password": "Admin Passord", diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index aab8e05b5ac..64e2298d141 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -3,20 +3,20 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 de dispositius externs del televisor per autoritzar Home Assistant.", "cannot_connect": "Ha fallat la connexi\u00f3", "id_missing": "El dispositiu Samsung no t\u00e9 cap n\u00famero de s\u00e8rie.", - "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible.", + "not_supported": "Actualment aquest dispositiu Samsung no \u00e9s compatible.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "error": { - "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant." + "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 de dispositius externs del televisor per autoritzar Home Assistant." }, "flow_title": "{device}", "step": { "confirm": { - "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.", + "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3.", "title": "Televisor Samsung" }, "reauth_confirm": { diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 3a0508099a8..17a4fcb20ca 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -13,10 +13,10 @@ "error": { "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV." }, - "flow_title": "{model}", + "flow_title": "{device}", "step": { "confirm": { - "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", + "description": "Wilt u Samsung TV {device} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", "title": "Samsung TV" }, "reauth_confirm": { diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json index a34ce4bdd0d..904d1542c8f 100644 --- a/homeassistant/components/sia/translations/ca.json +++ b/homeassistant/components/sia/translations/ca.json @@ -4,7 +4,7 @@ "invalid_account_format": "El compte no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", "invalid_account_length": "El compte no t\u00e9 la longitud correcta, ha de tenir entre 3 i 16 car\u00e0cters.", "invalid_key_format": "La clau no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", - "invalid_key_length": "La clau no t\u00e9 la longitud correcta, ha de tenir 16, 24 o 32 car\u00e0cters hexadecimals.", + "invalid_key_length": "La clau no t\u00e9 longitud correcta, ha de tenir 16, 24 o 32 car\u00e0cters hexadecimals.", "invalid_ping": "L'interval de refresc ha d'estar compr\u00e8s entre 1 i 1440 minuts.", "invalid_zones": "Cal que hi hagi com a m\u00ednim 1 zona.", "unknown": "Error inesperat" diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json index cd09ae7cdff..61ce61b7ee5 100644 --- a/homeassistant/components/sia/translations/no.json +++ b/homeassistant/components/sia/translations/no.json @@ -4,7 +4,7 @@ "invalid_account_format": "Kontoen er ikke en hex-verdi. Bruk bare 0-9 og AF.", "invalid_account_length": "Kontoen har ikke riktig lengde, den m\u00e5 v\u00e6re mellom 3 og 16 tegn.", "invalid_key_format": "N\u00f8kkelen er ikke en hex-verdi, bruk bare 0-9 og AF.", - "invalid_key_length": "N\u00f8kkelen har ikke riktig lengde, den m\u00e5 v\u00e6re p\u00e5 16, 24 eller 32 tegn med hex-tegn.", + "invalid_key_length": "N\u00f8kkelen har ikke riktig lengde, den m\u00e5 ha 16, 24 eller 32 hekser.", "invalid_ping": "Ping-intervallet m\u00e5 v\u00e6re mellom 1 og 1440 minutter.", "invalid_zones": "Det m\u00e5 v\u00e6re minst 1 sone.", "unknown": "Uventet feil" diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 9e2a6913fd4..cbe1d4e449c 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -14,7 +14,7 @@ "location": "Ubicaci\u00f3", "usercode": "Codi d'usuari" }, - "description": "Introdueix el codi d'usuari de l'usuari en aquesta ubicaci\u00f3", + "description": "Introdueix el codi de l'usuari en la ubicaci\u00f3 {location_id}", "title": "Codis d'usuari d'ubicaci\u00f3" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index 9c98d6ad1e7..839d901047b 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Plassering" + "location": "Plassering", + "usercode": "Brukerkode" }, - "description": "Angi brukerkoden for denne brukeren p\u00e5 denne plasseringen", + "description": "Angi brukerkoden for denne brukeren p\u00e5 plasseringen {location_id}", "title": "Brukerkoder for plassering" }, "reauth_confirm": { diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index f1049d6882e..ed0488f524d 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -36,7 +36,7 @@ "data": { "brightness_range_mode": "Helderheidsbereik gebruikt door apparaat", "curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)", - "max_kelvin": "Max ondersteunde kleurtemperatuur in kelvin", + "max_kelvin": "Max kleurtemperatuur in kelvin", "max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)", "min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin", "min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)", From 02cbb2025e21c850e8298061e8aa2614c82ff2de Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Sat, 29 May 2021 11:53:20 +1000 Subject: [PATCH 057/750] Decrease nsw fuel request volume (#49552) --- .../components/nsw_fuel_station/__init__.py | 63 +++++++ .../components/nsw_fuel_station/const.py | 3 + .../components/nsw_fuel_station/manifest.json | 2 +- .../components/nsw_fuel_station/sensor.py | 160 ++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../nsw_fuel_station/test_sensor.py | 133 ++++++++++----- 7 files changed, 218 insertions(+), 147 deletions(-) create mode 100644 homeassistant/components/nsw_fuel_station/const.py diff --git a/homeassistant/components/nsw_fuel_station/__init__.py b/homeassistant/components/nsw_fuel_station/__init__.py index 88ff1e779be..a79f164ba0f 100644 --- a/homeassistant/components/nsw_fuel_station/__init__.py +++ b/homeassistant/components/nsw_fuel_station/__init__.py @@ -1 +1,64 @@ """The nsw_fuel_station component.""" +from __future__ import annotations + +from dataclasses import dataclass +import datetime +import logging + +from nsw_fuel import FuelCheckClient, FuelCheckError, Station + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DATA_NSW_FUEL_STATION + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nsw_fuel_station" +SCAN_INTERVAL = datetime.timedelta(hours=1) + + +async def async_setup(hass, config): + """Set up the NSW Fuel Station platform.""" + client = FuelCheckClient() + + async def async_update_data(): + return await hass.async_add_executor_job(fetch_station_price_data, client) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sensor", + update_interval=SCAN_INTERVAL, + update_method=async_update_data, + ) + hass.data[DATA_NSW_FUEL_STATION] = coordinator + + await coordinator.async_refresh() + + return True + + +@dataclass +class StationPriceData: + """Data structure for O(1) price and name lookups.""" + + stations: dict[int, Station] + prices: dict[tuple[int, str], float] + + +def fetch_station_price_data(client: FuelCheckClient) -> StationPriceData | None: + """Fetch fuel price and station data.""" + try: + raw_price_data = client.get_fuel_prices() + # Restructure prices and station details to be indexed by station code + # for O(1) lookup + return StationPriceData( + stations={s.code: s for s in raw_price_data.stations}, + prices={ + (p.station_code, p.fuel_type): p.price for p in raw_price_data.prices + }, + ) + + except FuelCheckError as exc: + _LOGGER.error("Failed to fetch NSW Fuel station price data. %s", exc) + return None diff --git a/homeassistant/components/nsw_fuel_station/const.py b/homeassistant/components/nsw_fuel_station/const.py new file mode 100644 index 00000000000..885c8abf4a8 --- /dev/null +++ b/homeassistant/components/nsw_fuel_station/const.py @@ -0,0 +1,3 @@ +"""Constants for the NSW Fuel Station integration.""" + +DATA_NSW_FUEL_STATION = "nsw_fuel_station" diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index 4dca09e77ea..dfc6ad62d90 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -2,7 +2,7 @@ "domain": "nsw_fuel_station", "name": "NSW Fuel Station Price", "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", - "requirements": ["nsw-fuel-api-client==1.0.10"], + "requirements": ["nsw-fuel-api-client==1.1.0"], "codeowners": ["@nickw444"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 9522d6c430f..52536e69027 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -1,16 +1,21 @@ """Sensor platform to display the current fuel prices at a NSW fuel station.""" from __future__ import annotations -import datetime import logging -from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol +from homeassistant.components.nsw_fuel_station import ( + DATA_NSW_FUEL_STATION, + StationPriceData, +) from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) @@ -35,7 +40,6 @@ CONF_ALLOWED_FUEL_TYPES = [ CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] ATTRIBUTION = "Data provided by NSW Government FuelCheck" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_STATION_ID): cv.positive_int, @@ -45,11 +49,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(hours=1) - -NOTIFICATION_ID = "nsw_fuel_station_notification" -NOTIFICATION_TITLE = "NSW Fuel Station Sensor Setup" - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NSW Fuel Station sensor.""" @@ -57,122 +56,63 @@ def setup_platform(hass, config, add_entities, discovery_info=None): station_id = config[CONF_STATION_ID] fuel_types = config[CONF_FUEL_TYPES] - client = FuelCheckClient() - station_data = StationPriceData(client, station_id) - station_data.update() + coordinator = hass.data[DATA_NSW_FUEL_STATION] - if station_data.error is not None: - message = ("Error: {}. Check the logs for additional information.").format( - station_data.error - ) - - hass.components.persistent_notification.create( - message, title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID - ) + if coordinator.data is None: + _LOGGER.error("Initial fuel station price data not available") return - available_fuel_types = station_data.get_available_fuel_types() + entities = [] + for fuel_type in fuel_types: + if coordinator.data.prices.get((station_id, fuel_type)) is None: + _LOGGER.error( + "Fuel station price data not available for station %d and fuel type %s", + station_id, + fuel_type, + ) + continue - add_entities( - [ - StationPriceSensor(station_data, fuel_type) - for fuel_type in fuel_types - if fuel_type in available_fuel_types - ] - ) + entities.append(StationPriceSensor(coordinator, station_id, fuel_type)) + + add_entities(entities) -class StationPriceData: - """An object to store and fetch the latest data for a given station.""" - - def __init__(self, client, station_id: int) -> None: - """Initialize the sensor.""" - self.station_id = station_id - self._client = client - self._data = None - self._reference_data = None - self.error = None - self._station_name = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update the internal data using the API client.""" - - if self._reference_data is None: - try: - self._reference_data = self._client.get_reference_data() - except FuelCheckError as exc: - self.error = str(exc) - _LOGGER.error( - "Failed to fetch NSW Fuel station reference data. %s", exc - ) - return - - try: - self._data = self._client.get_fuel_prices_for_station(self.station_id) - except FuelCheckError as exc: - self.error = str(exc) - _LOGGER.error("Failed to fetch NSW Fuel station price data. %s", exc) - - def for_fuel_type(self, fuel_type: str): - """Return the price of the given fuel type.""" - if self._data is None: - return None - return next( - (price for price in self._data if price.fuel_type == fuel_type), None - ) - - def get_available_fuel_types(self): - """Return the available fuel types for the station.""" - return [price.fuel_type for price in self._data] - - def get_station_name(self) -> str: - """Return the name of the station.""" - if self._station_name is None: - name = None - if self._reference_data is not None: - name = next( - ( - station.name - for station in self._reference_data.stations - if station.code == self.station_id - ), - None, - ) - - self._station_name = name or f"station {self.station_id}" - - return self._station_name - - -class StationPriceSensor(SensorEntity): +class StationPriceSensor(CoordinatorEntity, SensorEntity): """Implementation of a sensor that reports the fuel price for a station.""" - def __init__(self, station_data: StationPriceData, fuel_type: str) -> None: + def __init__( + self, + coordinator: DataUpdateCoordinator[StationPriceData], + station_id: int, + fuel_type: str, + ) -> None: """Initialize the sensor.""" - self._station_data = station_data + super().__init__(coordinator) + + self._station_id = station_id self._fuel_type = fuel_type @property def name(self) -> str: """Return the name of the sensor.""" - return f"{self._station_data.get_station_name()} {self._fuel_type}" + station_name = self._get_station_name() + return f"{station_name} {self._fuel_type}" @property def state(self) -> float | None: """Return the state of the sensor.""" - price_info = self._station_data.for_fuel_type(self._fuel_type) - if price_info: - return price_info.price + if self.coordinator.data is None: + return None - return None + prices = self.coordinator.data.prices + return prices.get((self._station_id, self._fuel_type)) @property def extra_state_attributes(self) -> dict: """Return the state attributes of the device.""" return { - ATTR_STATION_ID: self._station_data.station_id, - ATTR_STATION_NAME: self._station_data.get_station_name(), + ATTR_STATION_ID: self._station_id, + ATTR_STATION_NAME: self._get_station_name(), ATTR_ATTRIBUTION: ATTRIBUTION, } @@ -181,6 +121,18 @@ class StationPriceSensor(SensorEntity): """Return the units of measurement.""" return f"{CURRENCY_CENT}/{VOLUME_LITERS}" - def update(self): - """Update current conditions.""" - self._station_data.update() + def _get_station_name(self): + default_name = f"station {self._station_id}" + if self.coordinator.data is None: + return default_name + + station = self.coordinator.data.stations.get(self._station_id) + if station is None: + return default_name + + return station.name + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{self._station_id}_{self._fuel_type}" diff --git a/requirements_all.txt b/requirements_all.txt index e4fb32e1c6f..7e207b9499f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1024,7 +1024,7 @@ notify-events==1.0.4 nsapi==3.0.4 # homeassistant.components.nsw_fuel_station -nsw-fuel-api-client==1.0.10 +nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat nuheat==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d383468e840..8f4e63fa0fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -560,7 +560,7 @@ nexia==0.9.7 notify-events==1.0.4 # homeassistant.components.nsw_fuel_station -nsw-fuel-api-client==1.0.10 +nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat nuheat==0.3.0 diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 40143a67bac..a9704256655 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -1,7 +1,10 @@ """The tests for the NSW Fuel Station sensor platform.""" from unittest.mock import patch +from nsw_fuel import FuelCheckError + from homeassistant.components import sensor +from homeassistant.components.nsw_fuel_station import DOMAIN from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -12,6 +15,8 @@ VALID_CONFIG = { "fuel_types": ["E10", "P95"], } +VALID_CONFIG_EXPECTED_ENTITY_IDS = ["my_fake_station_p95", "my_fake_station_e10"] + class MockPrice: """Mock Price implementation.""" @@ -34,68 +39,116 @@ class MockStation: self.code = code -class MockGetReferenceDataResponse: - """Mock GetReferenceDataResponse implementation.""" +class MockGetFuelPricesResponse: + """Mock GetFuelPricesResponse implementation.""" - def __init__(self, stations): - """Initialize a mock GetReferenceDataResponse instance.""" + def __init__(self, prices, stations): + """Initialize a mock GetFuelPricesResponse instance.""" + self.prices = prices self.stations = stations -class FuelCheckClientMock: - """Mock FuelCheckClient implementation.""" - - def get_fuel_prices_for_station(self, station): - """Return a fake fuel prices response.""" - return [ - MockPrice( - price=150.0, - fuel_type="P95", - last_updated=None, - price_unit=None, - station_code=350, - ), - MockPrice( - price=140.0, - fuel_type="E10", - last_updated=None, - price_unit=None, - station_code=350, - ), - ] - - def get_reference_data(self): - """Return a fake reference data response.""" - return MockGetReferenceDataResponse( - stations=[MockStation(code=350, name="My Fake Station")] - ) +MOCK_FUEL_PRICES_RESPONSE = MockGetFuelPricesResponse( + prices=[ + MockPrice( + price=150.0, + fuel_type="P95", + last_updated=None, + price_unit=None, + station_code=350, + ), + MockPrice( + price=140.0, + fuel_type="E10", + last_updated=None, + price_unit=None, + station_code=350, + ), + ], + stations=[MockStation(code=350, name="My Fake Station")], +) @patch( - "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", - new=FuelCheckClientMock, + "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices", + return_value=MOCK_FUEL_PRICES_RESPONSE, ) -async def test_setup(hass): +async def test_setup(get_fuel_prices, hass): """Test the setup with custom settings.""" + assert await async_setup_component(hass, DOMAIN, {}) with assert_setup_component(1, sensor.DOMAIN): assert await async_setup_component( hass, sensor.DOMAIN, {"sensor": VALID_CONFIG} ) await hass.async_block_till_done() - fake_entities = ["my_fake_station_p95", "my_fake_station_e10"] - - for entity_id in fake_entities: + for entity_id in VALID_CONFIG_EXPECTED_ENTITY_IDS: state = hass.states.get(f"sensor.{entity_id}") assert state is not None +def raise_fuel_check_error(): + """Raise fuel check error for testing error cases.""" + raise FuelCheckError() + + @patch( - "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", - new=FuelCheckClientMock, + "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices", + side_effect=raise_fuel_check_error, ) -async def test_sensor_values(hass): +async def test_setup_error(get_fuel_prices, hass): + """Test the setup with client throwing error.""" + assert await async_setup_component(hass, DOMAIN, {}) + with assert_setup_component(1, sensor.DOMAIN): + assert await async_setup_component( + hass, sensor.DOMAIN, {"sensor": VALID_CONFIG} + ) + await hass.async_block_till_done() + + for entity_id in VALID_CONFIG_EXPECTED_ENTITY_IDS: + state = hass.states.get(f"sensor.{entity_id}") + assert state is None + + +@patch( + "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices", + return_value=MOCK_FUEL_PRICES_RESPONSE, +) +async def test_setup_error_no_station(get_fuel_prices, hass): + """Test the setup with specified station not existing.""" + assert await async_setup_component(hass, DOMAIN, {}) + with assert_setup_component(2, sensor.DOMAIN): + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + "sensor": [ + { + "platform": "nsw_fuel_station", + "station_id": 350, + "fuel_types": ["E10"], + }, + { + "platform": "nsw_fuel_station", + "station_id": 351, + "fuel_types": ["P95"], + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.my_fake_station_e10") is not None + assert hass.states.get("sensor.my_fake_station_p95") is None + + +@patch( + "homeassistant.components.nsw_fuel_station.FuelCheckClient.get_fuel_prices", + return_value=MOCK_FUEL_PRICES_RESPONSE, +) +async def test_sensor_values(get_fuel_prices, hass): """Test retrieval of sensor values.""" + assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) await hass.async_block_till_done() From fb50cf9840e85a67f1bf6dbdcbde0ede584bf4fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 May 2021 21:18:59 -0500 Subject: [PATCH 058/750] Add network and callback support to SSDP (#51019) Co-authored-by: Ruslan Sayfutdinov Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + homeassistant/components/ssdp/__init__.py | 442 +++++++----- homeassistant/components/ssdp/descriptions.py | 70 ++ homeassistant/components/ssdp/flow.py | 50 ++ homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/package_constraints.txt | 1 - mypy.ini | 11 + requirements_all.txt | 1 - requirements_test_all.txt | 1 - tests/components/ssdp/test_init.py | 629 +++++++++++++----- tests/test_requirements.py | 6 +- 11 files changed, 900 insertions(+), 314 deletions(-) create mode 100644 homeassistant/components/ssdp/descriptions.py create mode 100644 homeassistant/components/ssdp/flow.py diff --git a/.strict-typing b/.strict-typing index e1c0d905621..e11374312da 100644 --- a/.strict-typing +++ b/.strict-typing @@ -61,6 +61,7 @@ homeassistant.components.scene.* homeassistant.components.sensor.* homeassistant.components.slack.* homeassistant.components.sonos.media_player +homeassistant.components.ssdp.* homeassistant.components.sun.* homeassistant.components.switch.* homeassistant.components.synology_dsm.* diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d9f74e5e776..5fcd58e14f7 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -4,18 +4,23 @@ from __future__ import annotations import asyncio from collections.abc import Mapping from datetime import timedelta +from ipaddress import IPv4Address, IPv6Address import logging -from typing import Any +from typing import Any, Callable -import aiohttp -from async_upnp_client.search import async_search -from defusedxml import ElementTree -from netdisco import ssdp, util +from async_upnp_client.search import SSDPListener +from async_upnp_client.utils import CaseInsensitiveDict +from homeassistant import config_entries +from homeassistant.components import network from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import CoreState, HomeAssistant, callback as core_callback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.loader import async_get_ssdp +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_ssdp, bind_hass + +from .descriptions import DescriptionManager +from .flow import FlowDispatcher, SSDPFlow DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) @@ -40,188 +45,321 @@ ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" + +DISCOVERY_MAPPING = { + "usn": ATTR_SSDP_USN, + "ext": ATTR_SSDP_EXT, + "server": ATTR_SSDP_SERVER, + "st": ATTR_SSDP_ST, + "location": ATTR_SSDP_LOCATION, +} + + _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): +@bind_hass +def async_register_callback( + hass: HomeAssistant, + callback: Callable[[dict], None], + match_dict: None | dict[str, str] = None, +) -> Callable[[], None]: + """Register to receive a callback on ssdp broadcast. + + Returns a callback that can be used to cancel the registration. + """ + scanner: Scanner = hass.data[DOMAIN] + return scanner.async_register_callback(callback, match_dict) + + +@bind_hass +def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name + hass: HomeAssistant, udn: str, st: str +) -> dict[str, str] | None: + """Fetch the discovery info cache.""" + scanner: Scanner = hass.data[DOMAIN] + return scanner.async_get_discovery_info_by_udn_st(udn, st) + + +@bind_hass +def async_get_discovery_info_by_st( # pylint: disable=invalid-name + hass: HomeAssistant, st: str +) -> list[dict[str, str]]: + """Fetch all the entries matching the st.""" + scanner: Scanner = hass.data[DOMAIN] + return scanner.async_get_discovery_info_by_st(st) + + +@bind_hass +def async_get_discovery_info_by_udn( + hass: HomeAssistant, udn: str +) -> list[dict[str, str]]: + """Fetch all the entries matching the udn.""" + scanner: Scanner = hass.data[DOMAIN] + return scanner.async_get_discovery_info_by_udn(udn) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SSDP integration.""" - async def _async_initialize(_): - scanner = Scanner(hass, await async_get_ssdp(hass)) - await scanner.async_scan(None) - cancel_scan = async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL) + scanner = hass.data[DOMAIN] = Scanner(hass, await async_get_ssdp(hass)) - @callback - def _async_stop_scans(event): - cancel_scan() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_scans) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) + asyncio.create_task(scanner.async_start()) return True +@core_callback +def _async_use_default_interface(adapters: list[network.Adapter]) -> bool: + for adapter in adapters: + if adapter["enabled"] and not adapter["default"]: + return False + return True + + +@core_callback +def _async_process_callbacks( + callbacks: list[Callable[[dict], None]], discovery_info: dict[str, str] +) -> None: + for callback in callbacks: + try: + callback(discovery_info) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to callback info: %s", discovery_info) + + class Scanner: """Class to manage SSDP scanning.""" - def __init__(self, hass, integration_matchers): + def __init__( + self, hass: HomeAssistant, integration_matchers: dict[str, list[dict[str, str]]] + ) -> None: """Initialize class.""" self.hass = hass - self.seen = set() - self._entries = [] + self.seen: set[tuple[str, str]] = set() + self.cache: dict[tuple[str, str], Mapping[str, str]] = {} self._integration_matchers = integration_matchers - self._description_cache = {} + self._cancel_scan: Callable[[], None] | None = None + self._ssdp_listeners: list[SSDPListener] = [] + self._callbacks: list[tuple[Callable[[dict], None], dict[str, str]]] = [] + self.flow_dispatcher: FlowDispatcher | None = None + self.description_manager: DescriptionManager | None = None - async def _on_ssdp_response(self, data: Mapping[str, Any]) -> None: - """Process an ssdp response.""" - self.async_store_entry( - ssdp.UPNPEntry({key.lower(): item for key, item in data.items()}) + @core_callback + def async_register_callback( + self, callback: Callable[[dict], None], match_dict: None | dict[str, str] = None + ) -> Callable[[], None]: + """Register a callback.""" + if match_dict is None: + match_dict = {} + + # Make sure any entries that happened + # before the callback was registered are fired + if self.hass.state != CoreState.running: + for headers in self.cache.values(): + self._async_callback_if_match(callback, headers, match_dict) + + callback_entry = (callback, match_dict) + self._callbacks.append(callback_entry) + + @core_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + return _async_remove_callback + + @core_callback + def _async_callback_if_match( + self, + callback: Callable[[dict], None], + headers: Mapping[str, str], + match_dict: dict[str, str], + ) -> None: + """Fire a callback if info matches the match dict.""" + if not all(headers.get(k) == v for (k, v) in match_dict.items()): + return + _async_process_callbacks( + [callback], self._async_headers_to_discovery_info(headers) ) - @callback - def async_store_entry(self, entry): - """Save an entry for later processing.""" - self._entries.append(entry) + @core_callback + def async_stop(self, *_: Any) -> None: + """Stop the scanner.""" + assert self._cancel_scan is not None + self._cancel_scan() + for listener in self._ssdp_listeners: + listener.async_stop() + self._ssdp_listeners = [] - async def async_scan(self, _): + async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]: + """Build the list of ssdp sources.""" + adapters = await network.async_get_adapters(self.hass) + sources: set[IPv4Address | IPv6Address] = set() + if _async_use_default_interface(adapters): + sources.add(IPv4Address("0.0.0.0")) + return sources + + for adapter in adapters: + if not adapter["enabled"]: + continue + if adapter["ipv4"]: + ipv4 = adapter["ipv4"][0] + sources.add(IPv4Address(ipv4["address"])) + if adapter["ipv6"]: + ipv6 = adapter["ipv6"][0] + # With python 3.9 add scope_ids can be + # added by enumerating adapter["ipv6"]s + # IPv6Address(f"::%{ipv6['scope_id']}") + sources.add(IPv6Address(ipv6["address"])) + + return sources + + @core_callback + def async_scan(self, *_: Any) -> None: """Scan for new entries.""" + for listener in self._ssdp_listeners: + listener.async_search() - await async_search(async_callback=self._on_ssdp_response) - await self._process_entries() - - # We clear the cache after each run. We track discovered entries - # so will never need a description twice. - self._description_cache.clear() - self._entries.clear() - - async def _process_entries(self): - """Process SSDP entries.""" - entries_to_process = [] - unseen_locations = set() - - for entry in self._entries: - key = (entry.st, entry.location) - - if key in self.seen: - continue - - self.seen.add(key) - - entries_to_process.append(entry) - - if ( - entry.location is not None - and entry.location not in self._description_cache - ): - unseen_locations.add(entry.location) - - if not entries_to_process: - return - - if unseen_locations: - await self._fetch_descriptions(list(unseen_locations)) - - tasks = [] - - for entry in entries_to_process: - info, domains = self._process_entry(entry) - for domain in domains: - _LOGGER.debug("Discovered %s at %s", domain, entry.location) - tasks.append( - self.hass.config_entries.flow.async_init( - domain, context={"source": DOMAIN}, data=info - ) + async def async_start(self) -> None: + """Start the scanner.""" + self.description_manager = DescriptionManager(self.hass) + self.flow_dispatcher = FlowDispatcher(self.hass) + for source_ip in await self._async_build_source_set(): + self._ssdp_listeners.append( + SSDPListener( + async_callback=self._async_process_entry, source_ip=source_ip ) - - if tasks: - await asyncio.gather(*tasks) - - async def _fetch_descriptions(self, locations): - """Fetch descriptions from locations.""" - - for idx, result in enumerate( - await asyncio.gather( - *[self._fetch_description(location) for location in locations], - return_exceptions=True, ) - ): - location = locations[idx] - if isinstance(result, Exception): - _LOGGER.exception( - "Failed to fetch ssdp data from: %s", location, exc_info=result - ) - continue + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self.flow_dispatcher.async_start + ) + await asyncio.gather( + *[listener.async_start() for listener in self._ssdp_listeners] + ) + self._cancel_scan = async_track_time_interval( + self.hass, self.async_scan, SCAN_INTERVAL + ) - self._description_cache[location] = result - - def _process_entry(self, entry): - """Process a single entry.""" - - info = {"st": entry.st} - for key in "usn", "ext", "server": - if key in entry.values: - info[key] = entry.values[key] - - if entry.location: - # Multiple entries usually share same location. Make sure - # we fetch it only once. - info_req = self._description_cache.get(entry.location) - if info_req is None: - return (None, []) - - info.update(info_req) + @core_callback + def _async_get_matching_callbacks( + self, headers: Mapping[str, str] + ) -> list[Callable[[dict], None]]: + """Return a list of callbacks that match.""" + return [ + callback + for callback, match_dict in self._callbacks + if all(headers.get(k) == v for (k, v) in match_dict.items()) + ] + @core_callback + def _async_matching_domains(self, info_with_req: CaseInsensitiveDict) -> set[str]: domains = set() for domain, matchers in self._integration_matchers.items(): for matcher in matchers: - if all(info.get(k) == v for (k, v) in matcher.items()): + if all(info_with_req.get(k) == v for (k, v) in matcher.items()): domains.add(domain) + return domains - if domains: - return (info_from_entry(entry, info), domains) + async def _async_process_entry(self, headers: Mapping[str, str]) -> None: + """Process SSDP entries.""" + _LOGGER.debug("_async_process_entry: %s", headers) + if "st" not in headers or "location" not in headers: + return + h_st = headers["st"] + h_location = headers["location"] + key = (h_st, h_location) - return (None, []) + if udn := _udn_from_usn(headers.get("usn")): + self.cache[(udn, h_st)] = headers - async def _fetch_description(self, xml_location): - """Fetch an XML description.""" - session = self.hass.helpers.aiohttp_client.async_get_clientsession() - try: - for _ in range(2): - resp = await session.get(xml_location, timeout=5) - xml = await resp.text(errors="replace") - # Samsung Smart TV sometimes returns an empty document the - # first time. Retry once. - if xml: - break - except (aiohttp.ClientError, asyncio.TimeoutError) as err: - _LOGGER.debug("Error fetching %s: %s", xml_location, err) - return {} + callbacks = self._async_get_matching_callbacks(headers) + if key in self.seen and not callbacks: + return - try: - tree = ElementTree.fromstring(xml) - except ElementTree.ParseError as err: - _LOGGER.debug("Error parsing %s: %s", xml_location, err) - return {} + assert self.description_manager is not None + info_req = await self.description_manager.fetch_description(h_location) or {} + info_with_req = CaseInsensitiveDict(**headers, **info_req) + discovery_info = discovery_info_from_headers_and_request(info_with_req) - return util.etree_to_dict(tree).get("root", {}).get("device", {}) + _async_process_callbacks(callbacks, discovery_info) + if key in self.seen: + return + self.seen.add(key) + + for domain in self._async_matching_domains(info_with_req): + _LOGGER.debug("Discovered %s at %s", domain, h_location) + flow: SSDPFlow = { + "domain": domain, + "context": {"source": config_entries.SOURCE_SSDP}, + "data": discovery_info, + } + assert self.flow_dispatcher is not None + self.flow_dispatcher.create(flow) + + @core_callback + def _async_headers_to_discovery_info( + self, headers: Mapping[str, str] + ) -> dict[str, str]: + """Combine the headers and description into discovery_info. + + Building this is a bit expensive so we only do it on demand. + """ + assert self.description_manager is not None + location = headers["location"] + info_req = self.description_manager.async_cached_description(location) or {} + return discovery_info_from_headers_and_request( + CaseInsensitiveDict(**headers, **info_req) + ) + + @core_callback + def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name + self, udn: str, st: str + ) -> dict[str, str] | None: + """Return discovery_info for a udn and st.""" + if headers := self.cache.get((udn, st)): + return self._async_headers_to_discovery_info(headers) + return None + + @core_callback + def async_get_discovery_info_by_st( # pylint: disable=invalid-name + self, st: str + ) -> list[dict[str, str]]: + """Return matching discovery_infos for a st.""" + return [ + self._async_headers_to_discovery_info(headers) + for udn_st, headers in self.cache.items() + if udn_st[1] == st + ] + + @core_callback + def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, str]]: + """Return matching discovery_infos for a udn.""" + return [ + self._async_headers_to_discovery_info(headers) + for udn_st, headers in self.cache.items() + if udn_st[0] == udn + ] -def info_from_entry(entry, device_info): - """Get info from an entry.""" - info = { - ATTR_SSDP_LOCATION: entry.location, - ATTR_SSDP_ST: entry.st, - } - if device_info: - info.update(device_info) - info.pop("st", None) - if "usn" in info: - info[ATTR_SSDP_USN] = info.pop("usn") - if "ext" in info: - info[ATTR_SSDP_EXT] = info.pop("ext") - if "server" in info: - info[ATTR_SSDP_SERVER] = info.pop("server") +def discovery_info_from_headers_and_request( + info_with_req: CaseInsensitiveDict, +) -> dict[str, str]: + """Convert headers and description to discovery_info.""" + info = {DISCOVERY_MAPPING.get(k.lower(), k): v for k, v in info_with_req.items()} + + if ATTR_UPNP_UDN not in info and ATTR_SSDP_USN in info: + if udn := _udn_from_usn(info[ATTR_SSDP_USN]): + info[ATTR_UPNP_UDN] = udn return info + + +def _udn_from_usn(usn: str | None) -> str | None: + """Get the UDN from the USN.""" + if usn is None: + return None + if usn.startswith("uuid:"): + return usn.split("::")[0] + return None diff --git a/homeassistant/components/ssdp/descriptions.py b/homeassistant/components/ssdp/descriptions.py new file mode 100644 index 00000000000..a6fda3685f2 --- /dev/null +++ b/homeassistant/components/ssdp/descriptions.py @@ -0,0 +1,70 @@ +"""The SSDP integration.""" +from __future__ import annotations + +import asyncio +import logging + +import aiohttp +from defusedxml import ElementTree +from netdisco import util + +from homeassistant.core import HomeAssistant, callback + +_LOGGER = logging.getLogger(__name__) + + +class DescriptionManager: + """Class to cache and manage fetching descriptions.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Init the manager.""" + self.hass = hass + self._description_cache: dict[str, None | dict[str, str]] = {} + + async def fetch_description( + self, xml_location: str | None + ) -> None | dict[str, str]: + """Fetch the location or get it from the cache.""" + if xml_location is None: + return None + if xml_location not in self._description_cache: + try: + self._description_cache[xml_location] = await self._fetch_description( + xml_location + ) + except Exception: # pylint: disable=broad-except + # If it fails, cache the failure so we do not keep trying over and over + self._description_cache[xml_location] = None + _LOGGER.exception("Failed to fetch ssdp data from: %s", xml_location) + + return self._description_cache[xml_location] + + @callback + def async_cached_description(self, xml_location: str) -> None | dict[str, str]: + """Fetch the description from the cache.""" + return self._description_cache[xml_location] + + async def _fetch_description(self, xml_location: str) -> None | dict[str, str]: + """Fetch an XML description.""" + session = self.hass.helpers.aiohttp_client.async_get_clientsession() + try: + for _ in range(2): + resp = await session.get(xml_location, timeout=5) + # Samsung Smart TV sometimes returns an empty document the + # first time. Retry once. + if xml := await resp.text(errors="replace"): + break + except (aiohttp.ClientError, asyncio.TimeoutError) as err: + _LOGGER.debug("Error fetching %s: %s", xml_location, err) + return None + + try: + tree = ElementTree.fromstring(xml) + except ElementTree.ParseError as err: + _LOGGER.debug("Error parsing %s: %s", xml_location, err) + return None + + parsed: dict[str, str] = ( + util.etree_to_dict(tree).get("root", {}).get("device", {}) + ) + return parsed diff --git a/homeassistant/components/ssdp/flow.py b/homeassistant/components/ssdp/flow.py new file mode 100644 index 00000000000..77f4cb107b8 --- /dev/null +++ b/homeassistant/components/ssdp/flow.py @@ -0,0 +1,50 @@ +"""The SSDP integration.""" +from __future__ import annotations + +from collections.abc import Coroutine +from typing import Any, TypedDict + +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult + + +class SSDPFlow(TypedDict): + """A queued ssdp discovery flow.""" + + domain: str + context: dict[str, Any] + data: dict + + +class FlowDispatcher: + """Dispatch discovery flows.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Init the discovery dispatcher.""" + self.hass = hass + self.pending_flows: list[SSDPFlow] = [] + self.started = False + + @callback + def async_start(self, *_: Any) -> None: + """Start processing pending flows.""" + self.started = True + self.hass.loop.call_soon(self._async_process_pending_flows) + + def _async_process_pending_flows(self) -> None: + for flow in self.pending_flows: + self.hass.async_create_task(self._init_flow(flow)) + self.pending_flows = [] + + def create(self, flow: SSDPFlow) -> None: + """Create and add or queue a flow.""" + if self.started: + self.hass.async_create_task(self._init_flow(flow)) + else: + self.pending_flows.append(flow) + + def _init_flow(self, flow: SSDPFlow) -> Coroutine[None, None, FlowResult]: + """Create a flow.""" + return self.hass.config_entries.flow.async_init( + flow["domain"], context=flow["context"], data=flow["data"] + ) diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index d3dbc0c920e..59c992cd34d 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,9 +4,9 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "netdisco==2.8.3", "async-upnp-client==0.18.0" ], + "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e6be2f0016..cfc0585fc84 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,6 @@ home-assistant-frontend==20210528.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 -netdisco==2.8.3 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 diff --git a/mypy.ini b/mypy.ini index 8de49a59259..43468d5b173 100644 --- a/mypy.ini +++ b/mypy.ini @@ -682,6 +682,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ssdp.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sun.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 7e207b9499f..4142f41a306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -993,7 +993,6 @@ nessclient==0.9.15 netdata==0.2.0 # homeassistant.components.discovery -# homeassistant.components.ssdp netdisco==2.8.3 # homeassistant.components.nam diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f4e63fa0fd..ceb880c0ccd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -547,7 +547,6 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.discovery -# homeassistant.components.ssdp netdisco==2.8.3 # homeassistant.components.nam diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 78b0f9e05b6..6dfdc02f6e7 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,42 +1,70 @@ """Test the SSDP integration.""" import asyncio from datetime import timedelta +from ipaddress import IPv4Address, IPv6Address from unittest.mock import patch import aiohttp +from async_upnp_client.search import SSDPListener +from async_upnp_client.utils import CaseInsensitiveDict import pytest from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, mock_coro -async def test_scan_match_st(hass, caplog): - """Test matching based on ST.""" - scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]}) +def _patched_ssdp_listener(info, *args, **kwargs): + listener = SSDPListener(*args, **kwargs) - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( - { - "st": "mock-st", - "location": None, - "usn": "mock-usn", - "server": "mock-server", - "ext": "", - } + async def _async_callback(*_): + await listener.async_callback(info) + + listener.async_start = _async_callback + return listener + + +async def _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp): + def _generate_fake_ssdp_listener(*args, **kwargs): + return _patched_ssdp_listener( + mock_ssdp_response, + *args, + **kwargs, ) with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: - await scanner.async_scan(None) + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + await hass.async_block_till_done() + + return mock_init + + +async def test_scan_match_st(hass, caplog): + """Test matching based on ST.""" + mock_ssdp_response = { + "st": "mock-st", + "location": None, + "usn": "mock-usn", + "server": "mock-server", + "ext": "", + } + mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]} + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -53,6 +81,19 @@ async def test_scan_match_st(hass, caplog): assert "Failed to fetch ssdp data" not in caplog.text +async def test_partial_response(hass, caplog): + """Test location and st missing.""" + mock_ssdp_response = { + "usn": "mock-usn", + "server": "mock-server", + "ext": "", + } + mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]} + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) + + assert len(mock_init.mock_calls) == 0 + + @pytest.mark.parametrize( "key", (ssdp.ATTR_UPNP_MANUFACTURER, ssdp.ATTR_UPNP_DEVICE_TYPE) ) @@ -68,25 +109,12 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): """, ) - scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]}) - - async def _mock_async_scan(*args, async_callback=None, **kwargs): - for _ in range(5): - await async_callback( - { - "st": "mock-st", - "location": "http://1.1.1.1", - } - ) - - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: - await scanner.async_scan(None) - + mock_get_ssdp = {"mock-domain": [{key: "Paulus"}]} + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) # If we get duplicate respones, ensure we only look it up once assert len(aioclient_mock.mock_calls) == 1 assert len(mock_init.mock_calls) == 1 @@ -108,33 +136,19 @@ async def test_scan_not_all_present(hass, aioclient_mock): """, ) - scanner = ssdp.Scanner( - hass, - { - "mock-domain": [ - { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", - } - ] - }, - ) - - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", } - ) - - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: - await scanner.async_scan(None) + ] + } + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) assert not mock_init.mock_calls @@ -152,33 +166,19 @@ async def test_scan_not_all_match(hass, aioclient_mock): """, ) - scanner = ssdp.Scanner( - hass, - { - "mock-domain": [ - { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus", - } - ] - }, - ) - - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus", } - ) - - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: - await scanner.async_scan(None) + ] + } + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) assert not mock_init.mock_calls @@ -187,21 +187,21 @@ async def test_scan_not_all_match(hass, aioclient_mock): async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): """Test failing to fetch description.""" aioclient_mock.get("http://1.1.1.1", exc=exc) - scanner = ssdp.Scanner(hass, {}) - - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", } - ) + ] + } + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ): - await scanner.async_scan(None) + assert not mock_init.mock_calls async def test_scan_description_parse_fail(hass, aioclient_mock): @@ -212,21 +212,22 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): INVALIDXML """, ) - scanner = ssdp.Scanner(hass, {}) - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", } - ) + ] + } + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ): - await scanner.async_scan(None) + assert not mock_init.mock_calls async def test_invalid_characters(hass, aioclient_mock): @@ -242,32 +243,20 @@ async def test_invalid_characters(hass, aioclient_mock): """, ) - scanner = ssdp.Scanner( - hass, - { - "mock-domain": [ - { - ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", - } - ] - }, - ) - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", } - ) + ] + } - with patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: - await scanner.async_scan(None) + mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -282,8 +271,9 @@ async def test_invalid_characters(hass, aioclient_mock): } -@patch("homeassistant.components.ssdp.async_search") -async def test_start_stop_scanner(async_search_mock, hass): +@patch("homeassistant.components.ssdp.SSDPListener.async_start") +@patch("homeassistant.components.ssdp.SSDPListener.async_search") +async def test_start_stop_scanner(async_start_mock, async_search_mock, hass): """Test we start and stop the scanner.""" assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) @@ -291,13 +281,15 @@ async def test_start_stop_scanner(async_search_mock, hass): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_search_mock.call_count == 2 + assert async_start_mock.call_count == 1 + assert async_search_mock.call_count == 1 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert async_search_mock.call_count == 2 + assert async_start_mock.call_count == 1 + assert async_search_mock.call_count == 1 async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog): @@ -313,34 +305,357 @@ async def test_unexpected_exception_while_fetching(hass, aioclient_mock, caplog) """, ) - scanner = ssdp.Scanner( - hass, - { - "mock-domain": [ - { - ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", - } - ] - }, - ) - - async def _mock_async_scan(*args, async_callback=None, **kwargs): - await async_callback( + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + } + mock_get_ssdp = { + "mock-domain": [ { - "st": "mock-st", - "location": "http://1.1.1.1", + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", } - ) + ] + } with patch( - "homeassistant.components.ssdp.ElementTree.fromstring", side_effect=ValueError - ), patch( - "homeassistant.components.ssdp.async_search", - side_effect=_mock_async_scan, - ), patch.object( - hass.config_entries.flow, "async_init", return_value=mock_coro() - ) as mock_init: - await scanner.async_scan(None) + "homeassistant.components.ssdp.descriptions.ElementTree.fromstring", + side_effect=ValueError, + ): + mock_init = await _async_run_mocked_scan( + hass, mock_ssdp_response, mock_get_ssdp + ) assert len(mock_init.mock_calls) == 0 assert "Failed to fetch ssdp data from: http://1.1.1.1" in caplog.text + + +async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): + """Test matching based on callback.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + Paulus + + + """, + ) + mock_ssdp_response = { + "st": "mock-st", + "location": "http://1.1.1.1", + "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + "server": "mock-server", + "ext": "", + } + not_matching_intergration_callbacks = [] + intergration_callbacks = [] + intergration_callbacks_from_cache = [] + match_any_callbacks = [] + + @callback + def _async_exception_callbacks(info): + raise ValueError + + @callback + def _async_intergration_callbacks(info): + intergration_callbacks.append(info) + + @callback + def _async_intergration_callbacks_from_cache(info): + intergration_callbacks_from_cache.append(info) + + @callback + def _async_not_matching_intergration_callbacks(info): + not_matching_intergration_callbacks.append(info) + + @callback + def _async_match_any_callbacks(info): + match_any_callbacks.append(info) + + def _generate_fake_ssdp_listener(*args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + await listener.async_callback(mock_ssdp_response) + + @callback + def _callback(*_): + hass.async_create_task(listener.async_callback(mock_ssdp_response)) + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, + ): + hass.state = CoreState.stopped + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + ssdp.async_register_callback(hass, _async_exception_callbacks, {}) + ssdp.async_register_callback( + hass, + _async_intergration_callbacks, + {"st": "mock-st"}, + ) + ssdp.async_register_callback( + hass, + _async_not_matching_intergration_callbacks, + {"st": "not-match-mock-st"}, + ) + ssdp.async_register_callback( + hass, + _async_match_any_callbacks, + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + ssdp.async_register_callback( + hass, + _async_intergration_callbacks_from_cache, + {"st": "mock-st"}, + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.state = CoreState.running + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert hass.state == CoreState.running + + assert len(intergration_callbacks) == 3 + assert len(intergration_callbacks_from_cache) == 3 + assert len(match_any_callbacks) == 3 + assert len(not_matching_intergration_callbacks) == 0 + assert intergration_callbacks[0] == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_SSDP_EXT: "", + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", + ssdp.ATTR_SSDP_SERVER: "mock-server", + ssdp.ATTR_SSDP_ST: "mock-st", + ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + assert "Failed to callback info" in caplog.text + + +async def test_scan_second_hit(hass, aioclient_mock, caplog): + """Test matching on second scan.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + Paulus + + + """, + ) + + mock_ssdp_response = CaseInsensitiveDict( + **{ + "ST": "mock-st", + "LOCATION": "http://1.1.1.1", + "USN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + "SERVER": "mock-server", + "EXT": "", + } + ) + mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]} + intergration_callbacks = [] + + @callback + def _async_intergration_callbacks(info): + intergration_callbacks.append(info) + + def _generate_fake_ssdp_listener(*args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + pass + + @callback + def _callback(*_): + hass.async_create_task(listener.async_callback(mock_ssdp_response)) + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + remove = ssdp.async_register_callback( + hass, + _async_intergration_callbacks, + {"st": "mock-st"}, + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + remove() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + + assert len(intergration_callbacks) == 2 + assert intergration_callbacks[0] == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_SSDP_EXT: "", + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", + ssdp.ATTR_SSDP_SERVER: "mock-server", + ssdp.ATTR_SSDP_ST: "mock-st", + ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_SSDP + } + assert mock_init.mock_calls[0][2]["data"] == { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_SSDP_ST: "mock-st", + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", + ssdp.ATTR_SSDP_SERVER: "mock-server", + ssdp.ATTR_SSDP_EXT: "", + ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + } + assert "Failed to fetch ssdp data" not in caplog.text + udn_discovery_info = ssdp.async_get_discovery_info_by_st(hass, "mock-st") + discovery_info = udn_discovery_info[0] + assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert ( + discovery_info[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + + st_discovery_info = ssdp.async_get_discovery_info_by_udn( + hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + discovery_info = st_discovery_info[0] + assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert ( + discovery_info[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + + discovery_info = ssdp.async_get_discovery_info_by_udn_st( + hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st" + ) + assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" + assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" + assert ( + discovery_info[ssdp.ATTR_UPNP_UDN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" + ) + assert ( + discovery_info[ssdp.ATTR_SSDP_USN] + == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" + ) + + assert ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None + + +_ADAPTERS_WITH_MANUAL_CONFIG = [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, +] + + +async def test_async_detect_interfaces_setting_empty_route(hass): + """Test without default interface config and the route returns nothing.""" + mock_get_ssdp = { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + } + create_args = [] + + def _generate_fake_ssdp_listener(*args, **kwargs): + create_args.append([args, kwargs]) + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + pass + + @callback + def _callback(*_): + pass + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value=mock_get_ssdp, + ), patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, + ), patch( + "homeassistant.components.ssdp.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ): + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert {create_args[0][1]["source_ip"], create_args[1][1]["source_ip"]} == { + IPv4Address("192.168.1.5"), + IPv6Address("2001:db8::"), + } diff --git a/tests/test_requirements.py b/tests/test_requirements.py index f68601e889e..26f3603910d 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -364,7 +364,11 @@ async def test_discovery_requirements_ssdp(hass): assert len(mock_process.mock_calls) == 4 assert mock_process.mock_calls[0][1][2] == ssdp.requirements # Ensure zeroconf is a dep for ssdp - assert mock_process.mock_calls[1][1][1] == "zeroconf" + assert { + mock_process.mock_calls[1][1][1], + mock_process.mock_calls[2][1][1], + mock_process.mock_calls[3][1][1], + } == {"network", "zeroconf", "http"} @pytest.mark.parametrize( From 84f0d3f961abd72340922c9e8c8f7cb40d4c11f6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 28 May 2021 21:32:50 -0500 Subject: [PATCH 059/750] Centralize Sonos subscription logic (#51172) * Centralize Sonos subscription logic * Clean up mocked Sonos Service instances, use subscription callback * Use existing mocked attributes * Use event dispatcher dict, move methods together, make update_alarms sync * Create dispatcher dict once --- homeassistant/components/sonos/favorites.py | 4 +- homeassistant/components/sonos/speaker.py | 158 ++++++++++---------- tests/components/sonos/conftest.py | 47 +++--- tests/components/sonos/test_sensor.py | 8 +- 4 files changed, 111 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 19dcb5184e5..2f5cab23be2 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -41,7 +41,9 @@ class SonosFavorites: Updated favorites are not always immediately available. """ - event_id = event.variables["favorites_update_id"] + if not (event_id := event.variables.get("favorites_update_id")): + return + if not self._event_version: self._event_version = event_id return diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 81c95f6e33f..079b916a4bc 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -61,6 +61,14 @@ EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } +SUBSCRIPTION_SERVICES = [ + "alarmClock", + "avTransport", + "contentDirectory", + "deviceProperties", + "renderingControl", + "zoneGroupTopology", +] UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} @@ -140,8 +148,12 @@ class SonosSpeaker: self.is_first_poll: bool = True self._is_ready: bool = False + + # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None + self._event_dispatchers: dict[str, Callable] = {} + self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None self._platforms_ready: set[str] = set() @@ -209,6 +221,15 @@ class SonosSpeaker: else: self._platforms_ready.add(SWITCH_DOMAIN) + self._event_dispatchers = { + "AlarmClock": self.async_dispatch_alarms, + "AVTransport": self.async_dispatch_media_update, + "ContentDirectory": self.favorites.async_delayed_update, + "DeviceProperties": self.async_dispatch_device_properties, + "RenderingControl": self.async_update_volume, + "ZoneGroupTopology": self.async_update_groups, + } + dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) async def async_handle_new_entity(self, entity_type: str) -> None: @@ -238,6 +259,9 @@ class SonosSpeaker: """Return whether this speaker is available.""" return self._seen_timer is not None + # + # Subscription handling and event dispatchers + # async def async_subscribe(self) -> bool: """Initiate event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) @@ -250,18 +274,11 @@ class SonosSpeaker: f"when existing subscriptions exist: {self._subscriptions}" ) - await asyncio.gather( - self._subscribe(self.soco.avTransport, self.async_update_media), - self._subscribe(self.soco.renderingControl, self.async_update_volume), - self._subscribe(self.soco.contentDirectory, self.async_update_content), - self._subscribe( - self.soco.zoneGroupTopology, self.async_dispatch_groups - ), - self._subscribe( - self.soco.deviceProperties, self.async_dispatch_properties - ), - self._subscribe(self.soco.alarmClock, self.async_dispatch_alarms), - ) + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + await asyncio.gather(*subscriptions) return True except SoCoException as ex: _LOGGER.warning("Could not connect %s: %s", self.zone_name, ex) @@ -279,26 +296,58 @@ class SonosSpeaker: self._subscriptions.append(subscription) @callback - def async_dispatch_properties(self, event: SonosEvent | None = None) -> None: - """Update properties from event.""" - self.hass.async_create_task(self.async_update_device_properties(event)) - - @callback - def async_dispatch_alarms(self, event: SonosEvent | None = None) -> None: - """Update alarms from event.""" - self.hass.async_create_task(self.async_update_alarms(event)) - - @callback - def async_dispatch_groups(self, event: SonosEvent | None = None) -> None: - """Update groups from event.""" - if event and self._poll_timer: + def async_dispatch_event(self, event: SonosEvent) -> None: + """Handle callback event and route as needed.""" + if self._poll_timer: _LOGGER.debug( "Received event, cancelling poll timer for %s", self.zone_name ) self._poll_timer() self._poll_timer = None - self.async_update_groups(event) + dispatcher = self._event_dispatchers[event.service.service_type] + dispatcher(event) + + @callback + def async_dispatch_alarms(self, event: SonosEvent) -> None: + """Create a task to update alarms from an event.""" + self.hass.async_add_executor_job(self.update_alarms) + + @callback + def async_dispatch_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + self.hass.async_create_task(self.async_update_device_properties(event)) + + async def async_update_device_properties(self, event: SonosEvent) -> None: + """Update device properties from an event.""" + if (more_info := event.variables.get("more_info")) is not None: + battery_dict = dict(x.split(":") for x in more_info.split(",")) + await self.async_update_battery_info(battery_dict) + self.async_write_entity_states() + + @callback + def async_dispatch_media_update(self, event: SonosEvent) -> None: + """Update information about currently playing media from an event.""" + self.hass.async_add_executor_job(self.update_media, event) + + @callback + def async_update_volume(self, event: SonosEvent) -> None: + """Update information about currently volume settings.""" + variables = event.variables + + if "volume" in variables: + self.volume = int(variables["volume"]["Master"]) + + if "mute" in variables: + self.muted = variables["mute"]["Master"] == "1" + + if "night_mode" in variables: + self.night_mode = variables["night_mode"] == "1" + + if "dialog_level" in variables: + self.dialog_mode = variables["dialog_level"] == "1" + + self.async_write_entity_states() async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" @@ -376,17 +425,6 @@ class SonosSpeaker: self._subscriptions = [] - async def async_update_device_properties(self, event: SonosEvent = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if (more_info := event.variables.get("more_info")) is not None: - battery_dict = dict(x.split(":") for x in more_info.split(",")) - await self.async_update_battery_info(battery_dict) - - self.async_write_entity_states() - def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -409,17 +447,11 @@ class SonosSpeaker: return new_alarms - async def async_update_alarms(self, event: SonosEvent | None = None) -> None: - """Update device properties using the provided SonosEvent.""" - if event is None: - return - - if new_alarms := await self.hass.async_add_executor_job( - self.update_alarms_for_speaker - ): - async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - - async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + def update_alarms(self) -> None: + """Update alarms from an event.""" + if new_alarms := self.update_alarms_for_speaker(): + dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + dispatcher_send(self.hass, SONOS_ALARM_UPDATE) async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" @@ -759,12 +791,6 @@ class SonosSpeaker: """Return the SonosFavorites instance for this household.""" return self.hass.data[DATA_SONOS].favorites[self.household_id] - @callback - def async_update_content(self, event: SonosEvent | None = None) -> None: - """Update information about available content.""" - if event and "favorites_update_id" in event.variables: - self.favorites.async_delayed_update(event) - def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -772,30 +798,6 @@ class SonosSpeaker: self.night_mode = self.soco.night_mode self.dialog_mode = self.soco.dialog_mode - @callback - def async_update_volume(self, event: SonosEvent) -> None: - """Update information about currently volume settings.""" - variables = event.variables - - if "volume" in variables: - self.volume = int(variables["volume"]["Master"]) - - if "mute" in variables: - self.muted = variables["mute"]["Master"] == "1" - - if "night_mode" in variables: - self.night_mode = variables["night_mode"] == "1" - - if "dialog_level" in variables: - self.dialog_mode = variables["dialog_level"] == "1" - - self.async_write_entity_states() - - @callback - def async_update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - self.hass.async_add_executor_job(self.update_media, event) - def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index aa14dcaa5cf..fc5ef84c2d6 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,15 +10,24 @@ from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry +class SonosMockService: + """Mock a Sonos Service used in callbacks.""" + + def __init__(self, service_type): + """Initialize the instance.""" + self.service_type = service_type + self.subscribe = AsyncMock() + + class SonosMockEvent: """Mock a sonos Event used in callbacks.""" - def __init__(self, soco, variables): + def __init__(self, soco, service, variables): """Initialize the instance.""" self.sid = f"{soco.uid}_sub0000000001" self.seq = "0" self.timestamp = 1621000000.0 - self.service = dummy_soco_service_fixture + self.service = service self.variables = variables @@ -29,9 +38,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture( - music_library, speaker_info, battery_info, dummy_soco_service, alarm_clock -): +def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -41,11 +48,11 @@ def soco_fixture( mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library mock_soco.get_speaker_info.return_value = speaker_info - mock_soco.avTransport = dummy_soco_service - mock_soco.renderingControl = dummy_soco_service - mock_soco.zoneGroupTopology = dummy_soco_service - mock_soco.contentDirectory = dummy_soco_service - mock_soco.deviceProperties = dummy_soco_service + mock_soco.avTransport = SonosMockService("AVTransport") + mock_soco.renderingControl = SonosMockService("RenderingControl") + mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") + mock_soco.contentDirectory = SonosMockService("ContentDirectory") + mock_soco.deviceProperties = SonosMockService("DeviceProperties") mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True @@ -74,14 +81,6 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}} -@pytest.fixture(name="dummy_soco_service") -def dummy_soco_service_fixture(): - """Create dummy_soco_service fixture.""" - service = Mock() - service.subscribe = AsyncMock() - return service - - @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" @@ -93,8 +92,8 @@ def music_library_fixture(): @pytest.fixture(name="alarm_clock") def alarm_clock_fixture(): """Create alarmClock fixture.""" - alarm_clock = Mock() - alarm_clock.subscribe = AsyncMock() + alarm_clock = SonosMockService("AlarmClock") + alarm_clock.ListAlarms = Mock() alarm_clock.ListAlarms.return_value = { "CurrentAlarmList": "" '" ' Date: Fri, 28 May 2021 23:28:07 -0500 Subject: [PATCH 060/750] Fix use of async in Sonos switch (#51210) * Fix use of async in Sonos switch * Simplify * Convert to callback --- homeassistant/components/sonos/switch.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 2038002db2d..4b24224f6a0 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -8,6 +8,7 @@ from pysonos.exceptions import SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ATTR_TIME +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -99,7 +100,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): str(self.alarm.start_time)[0:5], ) - async def async_check_if_available(self): + @callback + def async_check_if_available(self): """Check if alarm exists and remove alarm entity if not available.""" if self.alarm_id in self.hass.data[DATA_SONOS].alarms: return True @@ -114,11 +116,9 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): async def async_update(self) -> None: """Poll the device for the current state.""" - if await self.async_check_if_available(): - await self.hass.async_add_executor_job(self.update_alarm) + if not self.async_check_if_available(): + return - def update_alarm(self): - """Update the state of the alarm.""" _LOGGER.debug("Updating alarm: %s", self.entity_id) if self.speaker.soco.uid != self.alarm.zone.uid: self.speaker = self.hass.data[DATA_SONOS].discovered.get( @@ -129,11 +129,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): "No configured Sonos speaker has been found to match the alarm." ) - self._update_device() + self._async_update_device() - self.schedule_update_ha_state() + self.async_write_ha_state() - def _update_device(self): + @callback + def _async_update_device(self): """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) From 4d428b87cb4cf4bcad6008901bb1df1d14d63979 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 29 May 2021 14:06:02 +0200 Subject: [PATCH 061/750] Remove incorrect check in Alexa for SERVICE_ALARM_DISARM fail (#51224) --- homeassistant/components/alexa/handlers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index da0011f817a..79e322b4ea7 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -62,7 +62,6 @@ from .errors import ( AlexaInvalidDirectiveError, AlexaInvalidValueError, AlexaSecurityPanelAuthorizationRequired, - AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, AlexaVideoActionNotPermittedForContentError, @@ -927,11 +926,9 @@ async def async_api_disarm(hass, config, directive, context): if payload["authorization"]["type"] == "FOUR_DIGIT_PIN": data["code"] = value - if not await hass.services.async_call( + await hass.services.async_call( entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context - ): - msg = "Invalid Code" - raise AlexaSecurityPanelUnauthorizedError(msg) + ) response.add_context_property( { From b6716ecebdfa674b0653b265e93e1a5274ee7654 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 29 May 2021 14:06:56 +0200 Subject: [PATCH 062/750] Add discovery by manufacturer to Nettigo Air Monitor integration (#51155) --- homeassistant/components/nam/manifest.json | 13 +++++++++++-- homeassistant/generated/zeroconf.py | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 3e03a0ad787..a003f3948e2 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -4,8 +4,17 @@ "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], "requirements": ["nettigo-air-monitor==0.2.6"], - "zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}], + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "nam-*" + }, + { + "type": "_http._tcp.local.", + "manufacturer": "nettigo" + } + ], "config_flow": true, "quality_scale": "platinum", "iot_class": "local_polling" -} +} \ No newline at end of file diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 014edc4b1f3..125ff34206a 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -108,6 +108,10 @@ ZEROCONF = { "domain": "nam", "name": "nam-*" }, + { + "domain": "nam", + "manufacturer": "nettigo" + }, { "domain": "rachio", "name": "rachio*" From c2f5dcefa5249856f147a442b32ff3e16a028be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 29 May 2021 15:09:13 +0300 Subject: [PATCH 063/750] Use flow result type constants more (#51122) --- homeassistant/components/auth/login_flow.py | 2 +- homeassistant/components/mqtt/discovery.py | 3 ++- homeassistant/components/mysensors/config_flow.py | 4 ++-- .../templates/config_flow/tests/test_config_flow.py | 9 +++++---- tests/auth/providers/test_trusted_networks.py | 13 +++++++------ tests/test_config_entries.py | 11 ++++++----- tests/test_data_entry_flow.py | 4 ++-- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 725450a0a12..c951e652356 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -52,7 +52,7 @@ flow for details. Progress the flow. Most flows will be 1 page, but could optionally add extra login challenges, like TFA. Once the flow has finished, the returned step will -have type "create_entry" and "result" key will contain an authorization code. +have type RESULT_TYPE_CREATE_ENTRY and "result" key will contain an authorization code. The authorization code associated with an authorized user by default, it will associate with an credential if "type" set to "link_user" in "/auth/login_flow" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3a5a3cb5f87..e68b47abe02 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -9,6 +9,7 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -274,7 +275,7 @@ async def async_start( # noqa: C901 ) if ( result - and result["type"] == "abort" + and result["type"] == RESULT_TYPE_ABORT and result["reason"] in ["already_configured", "single_instance_allowed"] ): diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 6676e11febf..ad260c3ab58 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.mysensors import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import RESULT_TYPE_FORM, FlowResult import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -132,7 +132,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL result: dict[str, Any] = await self.async_step_user(user_input=user_input) - if result["type"] == "form": + if result["type"] == RESULT_TYPE_FORM: return self.async_abort(reason=next(iter(result["errors"].values()))) return result 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 674cb921cdd..e72d9eb7679 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -5,6 +5,7 @@ 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 homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM async def test_form(hass: HomeAssistant) -> None: @@ -13,7 +14,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -33,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "1.1.1.1", @@ -62,7 +63,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -85,5 +86,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == "form" + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 39764fa4206..412f660adc3 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY @pytest.fixture @@ -161,7 +162,7 @@ async def test_login_flow(manager, provider): # not from trusted network flow = await provider.async_login_flow({"ip_address": ip_address("127.0.0.1")}) step = await flow.async_step_init() - assert step["type"] == "abort" + assert step["type"] == RESULT_TYPE_ABORT assert step["reason"] == "not_allowed" # from trusted network, list users @@ -176,7 +177,7 @@ async def test_login_flow(manager, provider): # login with valid user step = await flow.async_step_init({"user": user.id}) - assert step["type"] == "create_entry" + assert step["type"] == RESULT_TYPE_CREATE_ENTRY assert step["data"]["user"] == user.id @@ -200,7 +201,7 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == "abort" + assert step["type"] == RESULT_TYPE_ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -284,7 +285,7 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == "abort" + assert step["type"] == RESULT_TYPE_ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -322,7 +323,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == "abort" + assert step["type"] == RESULT_TYPE_ABORT assert step["reason"] == "not_allowed" # from trusted network, only one available user, bypass the login flow @@ -330,7 +331,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("192.168.0.1")} ) step = await flow.async_step_init() - assert step["type"] == "create_entry" + assert step["type"] == RESULT_TYPE_CREATE_ENTRY assert step["data"]["user"] == owner.id user = await manager_bypass_login.async_create_user("test-user") diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e9e864c4491..18791b1eb2d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -9,6 +9,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, callback +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -1612,7 +1613,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1657,7 +1658,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1674,7 +1675,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "2.2.2.2" assert entry.data["additional"] == "data" @@ -1717,7 +1718,7 @@ async def test_unique_id_not_update_existing_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "0.0.0.0" assert entry.data["additional"] == "data" @@ -2861,5 +2862,5 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == reason diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 34b07a2a871..4b5777d86f8 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -121,7 +121,7 @@ async def test_show_form(manager): ) form = await manager.async_init("test") - assert form["type"] == "form" + assert form["type"] == data_entry_flow.RESULT_TYPE_FORM assert form["data_schema"] is schema assert form["errors"] == {"username": "Should be unique."} @@ -369,7 +369,7 @@ async def test_abort_flow_exception(manager): raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) form = await manager.async_init("test") - assert form["type"] == "abort" + assert form["type"] == data_entry_flow.RESULT_TYPE_ABORT assert form["reason"] == "mock-reason" assert form["description_placeholders"] == {"placeholder": "yo"} From d1132270b4bb0710cbe40399e66ca3cf2b58c832 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 May 2021 07:09:49 -0500 Subject: [PATCH 064/750] Remove double schema validation in network (#51219) --- homeassistant/components/network/network.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index 1243ba24774..ffe3406e28e 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant, callback from .const import ( ATTR_CONFIGURED_ADAPTERS, DEFAULT_CONFIGURED_ADAPTERS, - NETWORK_CONFIG_SCHEMA, STORAGE_KEY, STORAGE_VERSION, ) @@ -63,7 +62,6 @@ class Network: async def async_reconfig(self, config: dict[str, Any]) -> None: """Reconfigure network.""" - config = NETWORK_CONFIG_SCHEMA(config) self._data[ATTR_CONFIGURED_ADAPTERS] = config[ATTR_CONFIGURED_ADAPTERS] self.async_configure() await self._async_save() From d66d7cbd3754ecaa6d51c0bade7d6d752849fc99 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 29 May 2021 14:10:45 +0200 Subject: [PATCH 065/750] Fix Netatmo data class update (#51215) * Catch if data class entry is None * Guard --- homeassistant/components/netatmo/data_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 12376e5ac78..1c092c40930 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -101,8 +101,7 @@ class NetatmoDataHandler: time() + data_class["interval"] ) - if self.data_classes[data_class_name]["subscriptions"]: - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(data_class_name) self._queue.rotate(BATCH_SIZE) @@ -133,6 +132,9 @@ class NetatmoDataHandler: async def async_fetch_data(self, data_class_entry): """Fetch data and notify.""" + if self.data[data_class_entry] is None: + return + try: await self.data[data_class_entry].async_update() From 255e13930c24f80a7eb028445ea2ef6bdb1716c9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 May 2021 14:35:02 +0200 Subject: [PATCH 066/750] Define CoverEntity entity attributes as class variables (#51236) * Define CoverEntity entity attributes as class variables * Fix supported features --- homeassistant/components/cover/__init__.py | 33 ++++++++++++++++------ homeassistant/components/zwave_js/cover.py | 2 +- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 034beb7f9db..60fd5a2af2c 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -1,4 +1,6 @@ """Support for Cover devices.""" +from __future__ import annotations + from datetime import timedelta import functools as ft import logging @@ -167,22 +169,32 @@ async def async_unload_entry(hass, entry): class CoverEntity(Entity): """Base class for cover entities.""" + _attr_current_cover_position: int | None = None + _attr_current_cover_tilt_position: int | None = None + _attr_is_closed: bool | None + _attr_is_closing: bool | None = None + _attr_is_opening: bool | None = None + _attr_state: None = None + @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. """ + return self._attr_current_cover_position @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. None is unknown, 0 is closed, 100 is fully open. """ + return self._attr_current_cover_tilt_position @property - def state(self): + @final + def state(self) -> str | None: """Return the state of the cover.""" if self.is_opening: return STATE_OPENING @@ -213,8 +225,11 @@ class CoverEntity(Entity): return data @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" + if self._attr_supported_features is not None: + return self._attr_supported_features + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP if self.current_cover_position is not None: @@ -231,17 +246,19 @@ class CoverEntity(Entity): return supported_features @property - def is_opening(self): + def is_opening(self) -> bool | None: """Return if the cover is opening or not.""" + return self._attr_is_opening @property - def is_closing(self): + def is_closing(self) -> bool | None: """Return if the cover is closing or not.""" + return self._attr_is_closing @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" - raise NotImplementedError() + return self._attr_is_closed def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 302ccd9cd32..2adc64d528f 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -147,7 +147,7 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): ) @property - def supported_features(self) -> int | None: + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE From c5e5787e1d72334afd98d33c104b1dc0000133e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 May 2021 08:31:22 -0500 Subject: [PATCH 067/750] Replace sonos discovery thread with ssdp callback registration (#51033) Co-authored-by: jjlawren --- homeassistant/components/sonos/__init__.py | 157 +++++++++++-------- homeassistant/components/sonos/const.py | 2 + homeassistant/components/sonos/manifest.json | 1 + tests/components/sonos/conftest.py | 15 +- 4 files changed, 108 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index a904ae58db6..3b158a7db81 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -6,6 +6,7 @@ from collections import OrderedDict import datetime import logging import socket +from urllib.parse import urlparse import pysonos from pysonos import events_asyncio @@ -15,6 +16,7 @@ from pysonos.exceptions import SoCoException import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -24,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( DATA_SONOS, @@ -34,6 +36,7 @@ from .const import ( SONOS_ALARM_UPDATE, SONOS_GROUP_UPDATE, SONOS_SEEN, + UPNP_ST, ) from .favorites import SonosFavorites from .speaker import SonosSpeaker @@ -74,7 +77,6 @@ class SonosData: self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, Alarm] = {} self.topology_condition = asyncio.Condition() - self.discovery_thread = None self.hosts_heartbeat = None @@ -94,89 +96,101 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up Sonos from a config entry.""" pysonos.config.EVENTS_MODULE = events_asyncio if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() + data = hass.data[DATA_SONOS] config = hass.data[DOMAIN].get("media_player", {}) + hosts = config.get(CONF_HOSTS, []) + discovery_lock = asyncio.Lock() _LOGGER.debug("Reached async_setup_entry, config=%s", config) advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - def _stop_discovery(event: Event) -> None: - data = hass.data[DATA_SONOS] - if data.discovery_thread: - data.discovery_thread.stop() - data.discovery_thread = None + async def _async_stop_event_listener(event: Event) -> None: + if events_asyncio.event_listener: + await events_asyncio.event_listener.async_stop() + + def _stop_manual_heartbeat(event: Event) -> None: if data.hosts_heartbeat: data.hosts_heartbeat() data.hosts_heartbeat = None - def _discovery(now: datetime.datetime | None = None) -> None: - """Discover players from network or configuration.""" - hosts = config.get(CONF_HOSTS) + def _discovered_player(soco: SoCo) -> None: + """Handle a (re)discovered player.""" + try: + _LOGGER.debug("Reached _discovered_player, soco=%s", soco) + speaker_info = soco.get_speaker_info(True) + _LOGGER.debug("Adding new speaker: %s", speaker_info) + speaker = SonosSpeaker(hass, soco, speaker_info) + data.discovered[soco.uid] = speaker + if soco.household_id not in data.favorites: + data.favorites[soco.household_id] = SonosFavorites( + hass, soco.household_id + ) + data.favorites[soco.household_id].update() + speaker.setup() + except SoCoException as ex: + _LOGGER.debug("SoCoException, ex=%s", ex) - def _discovered_player(soco: SoCo) -> None: - """Handle a (re)discovered player.""" + def _manual_hosts(now: datetime.datetime | None = None) -> None: + """Players from network configuration.""" + for host in hosts: try: - _LOGGER.debug("Reached _discovered_player, soco=%s", soco) + _LOGGER.debug("Testing %s", host) + player = pysonos.SoCo(socket.gethostbyname(host)) + if player.is_visible: + # Make sure that the player is available + _ = player.volume + _discovered_player(player) + except (OSError, SoCoException) as ex: + _LOGGER.debug("Issue connecting to '%s': %s", host, ex) + if now is None: + _LOGGER.warning("Failed to initialize '%s'", host) - data = hass.data[DATA_SONOS] - - if soco.uid not in data.discovered: - speaker_info = soco.get_speaker_info(True) - _LOGGER.debug("Adding new speaker: %s", speaker_info) - speaker = SonosSpeaker(hass, soco, speaker_info) - data.discovered[soco.uid] = speaker - if soco.household_id not in data.favorites: - data.favorites[soco.household_id] = SonosFavorites( - hass, soco.household_id - ) - data.favorites[soco.household_id].update() - speaker.setup() - else: - dispatcher_send(hass, f"{SONOS_SEEN}-{soco.uid}", soco) - - except SoCoException as ex: - _LOGGER.debug("SoCoException, ex=%s", ex) - - if hosts: - for host in hosts: - try: - _LOGGER.debug("Testing %s", host) - player = pysonos.SoCo(socket.gethostbyname(host)) - if player.is_visible: - # Make sure that the player is available - _ = player.volume - - _discovered_player(player) - except (OSError, SoCoException) as ex: - _LOGGER.debug("Exception %s", ex) - if now is None: - _LOGGER.warning("Failed to initialize '%s'", host) - - _LOGGER.debug("Tested all hosts") - hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later( - DISCOVERY_INTERVAL.total_seconds(), _discovery - ) - else: - _LOGGER.debug("Starting discovery thread") - hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread( - _discovered_player, - interval=DISCOVERY_INTERVAL.total_seconds(), - interface_addr=config.get(CONF_INTERFACE_ADDR), - ) - hass.data[DATA_SONOS].discovery_thread.name = "Sonos-Discovery" + _LOGGER.debug("Tested all hosts") + data.hosts_heartbeat = hass.helpers.event.call_later( + DISCOVERY_INTERVAL.total_seconds(), _manual_hosts + ) @callback def _async_signal_update_groups(event): async_dispatcher_send(hass, SONOS_GROUP_UPDATE) + def _discovered_ip(ip_address): + try: + player = pysonos.SoCo(ip_address) + except (OSError, SoCoException): + _LOGGER.debug("Failed to connect to discovered player '%s'", ip_address) + return + if player.is_visible: + _discovered_player(player) + + async def _async_create_discovered_player(uid, discovered_ip): + """Only create one player at a time.""" + async with discovery_lock: + if uid in data.discovered: + async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}") + return + await hass.async_add_executor_job(_discovered_ip, discovered_ip) + + @callback + def _async_discovered_player(info): + _LOGGER.debug("Sonos Discovery: %s", info) + uid = info.get(ssdp.ATTR_UPNP_UDN) + if uid.startswith("uuid:"): + uid = uid[5:] + discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname + asyncio.create_task(_async_create_discovered_player(uid, discovered_ip)) + @callback def _async_signal_update_alarms(event): async_dispatcher_send(hass, SONOS_ALARM_UPDATE) @@ -188,9 +202,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS ] ) - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) - ) entry.async_on_unload( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, _async_signal_update_groups @@ -201,8 +212,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENT_HOMEASSISTANT_START, _async_signal_update_alarms ) ) + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener + ) + ) _LOGGER.debug("Adding discovery job") - await hass.async_add_executor_job(_discovery) + if hosts: + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat + ) + ) + await hass.async_add_executor_job(_manual_hosts) + return + + entry.async_on_unload( + ssdp.async_register_callback( + hass, _async_discovered_player, {"st": UPNP_ST} + ) + ) hass.async_create_task(setup_platforms_and_discovery()) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 0a70844e6b5..d32dda6a53b 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -22,6 +22,8 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" + DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" PLATFORMS = {BINARY_SENSOR_DOMAIN, MP_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN} diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e42937d3889..bb949fea8c1 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": ["pysonos==0.0.50"], + "dependencies": ["ssdp"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index fc5ef84c2d6..81ad0e6c8ef 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch as patch import pytest +from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS @@ -44,6 +45,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): "socket.gethostbyname", return_value="192.168.42.2" ): mock_soco = mock.return_value + mock_soco.ip_address = "192.168.42.2" mock_soco.uid = "RINCON_test" mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library @@ -67,11 +69,18 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): def discover_fixture(soco): """Create a mock pysonos discover fixture.""" - def do_callback(callback, **kwargs): - callback(soco) + def do_callback(hass, callback, *args, **kwargs): + callback( + { + ssdp.ATTR_UPNP_UDN: soco.uid, + ssdp.ATTR_SSDP_LOCATION: f"http://{soco.ip_address}/", + } + ) return MagicMock() - with patch("pysonos.discover_thread", side_effect=do_callback) as mock: + with patch( + "homeassistant.components.ssdp.async_register_callback", side_effect=do_callback + ) as mock: yield mock From 27b9d7fed0c1578e4954d2aae31ffdaf253dfa12 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 29 May 2021 16:00:36 +0200 Subject: [PATCH 068/750] Fix flaky statistics tests (#51242) --- tests/components/history/test_init.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 36dd3f30156..bf8d34e6ffe 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ import json from unittest.mock import patch, sentinel import pytest +from pytest import approx from homeassistant.components import history, recorder from homeassistant.components.recorder.history import get_significant_states @@ -884,9 +885,9 @@ async def test_statistics_during_period(hass, hass_ws_client): { "statistic_id": "sensor.test", "start": now.isoformat(), - "mean": 10.0, - "min": 10.0, - "max": 10.0, + "mean": approx(10.0), + "min": approx(10.0), + "max": approx(10.0), "last_reset": None, "state": None, "sum": None, From 3d2f696d73f0e9c8d26f1e3062e007d3d955b5bf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 29 May 2021 09:08:46 -0500 Subject: [PATCH 069/750] Reorganize SonosSpeaker class for readability (#51222) --- homeassistant/components/sonos/speaker.py | 112 ++++++++++++++-------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 079b916a4bc..701fe5aa8c4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -146,36 +146,43 @@ class SonosSpeaker: self.household_id: str = soco.household_id self.media = SonosMedia(soco) + # Synchronization helpers self.is_first_poll: bool = True self._is_ready: bool = False + self._platforms_ready: set[str] = set() # Subscriptions and events self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} + # Scheduled callback handles self._poll_timer: Callable | None = None self._seen_timer: Callable | None = None - self._platforms_ready: set[str] = set() + # Dispatcher handles self._entity_creation_dispatcher: Callable | None = None self._group_dispatcher: Callable | None = None self._seen_dispatcher: Callable | None = None + # Device information self.mac_address = speaker_info["mac_address"] self.model_name = speaker_info["model_name"] self.version = speaker_info["display_version"] self.zone_name = speaker_info["zone_name"] + # Battery self.battery_info: dict[str, Any] | None = None self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None + # Volume / Sound self.volume: int | None = None self.muted: bool | None = None self.night_mode: bool | None = None self.dialog_mode: bool | None = None + # Grouping self.coordinator: SonosSpeaker | None = None self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] @@ -232,6 +239,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) + # + # Entity management + # async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" self._platforms_ready.add(entity_type) @@ -254,11 +264,32 @@ class SonosSpeaker: self.media.play_mode = self.soco.play_mode self.update_volume() + # + # Properties + # @property def available(self) -> bool: """Return whether this speaker is available.""" return self._seen_timer is not None + @property + def favorites(self) -> SonosFavorites: + """Return the SonosFavorites instance for this household.""" + return self.hass.data[DATA_SONOS].favorites[self.household_id] + + @property + def is_coordinator(self) -> bool: + """Return true if player is a coordinator.""" + return self.coordinator is None + + @property + def subscription_address(self) -> str | None: + """Return the current subscription callback address if any.""" + if self._subscriptions: + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) + return None + # # Subscription handling and event dispatchers # @@ -295,6 +326,30 @@ class SonosSpeaker: subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) + @callback + def async_renew_failed(self, exception: Exception) -> None: + """Handle a failed subscription renewal.""" + self.hass.async_create_task(self.async_resubscribe(exception)) + + async def async_resubscribe(self, exception: Exception) -> None: + """Attempt to resubscribe when a renewal failure is detected.""" + async with self._resubscription_lock: + if not self.available: + return + + if getattr(exception, "status", None) == 412: + _LOGGER.warning( + "Subscriptions for %s failed, speaker may have lost power", + self.zone_name, + ) + else: + _LOGGER.error( + "Subscription renewals for %s failed", + self.zone_name, + exc_info=exception, + ) + await self.async_unseen() + @callback def async_dispatch_event(self, event: SonosEvent) -> None: """Handle callback event and route as needed.""" @@ -349,6 +404,9 @@ class SonosSpeaker: self.async_write_entity_states() + # + # Speaker availability methods + # async def async_seen(self, soco: SoCo | None = None) -> None: """Record that this speaker was seen right now.""" if soco is not None: @@ -386,28 +444,6 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_resubscribe(self, exception: Exception) -> None: - """Attempt to resubscribe when a renewal failure is detected.""" - async with self._resubscription_lock: - if self.available: - if getattr(exception, "status", None) == 412: - _LOGGER.warning( - "Subscriptions for %s failed, speaker may have lost power", - self.zone_name, - ) - else: - _LOGGER.error( - "Subscription renewals for %s failed", - self.zone_name, - exc_info=exception, - ) - await self.async_unseen() - - @callback - def async_renew_failed(self, exception: Exception) -> None: - """Handle a failed subscription renewal.""" - self.hass.async_create_task(self.async_resubscribe(exception)) - async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self.async_write_entity_states() @@ -425,6 +461,9 @@ class SonosSpeaker: self._subscriptions = [] + # + # Alarm management + # def update_alarms_for_speaker(self) -> set[str]: """Update current alarm instances. @@ -453,6 +492,9 @@ class SonosSpeaker: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) dispatcher_send(self.hass, SONOS_ALARM_UPDATE) + # + # Battery management + # async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: """Update battery info using the decoded SonosEvent.""" self._last_battery_event = dt_util.utcnow() @@ -477,11 +519,6 @@ class SonosSpeaker: ): self.battery_info = battery_info - @property - def is_coordinator(self) -> bool: - """Return true if player is a coordinator.""" - return self.coordinator is None - @property def power_source(self) -> str | None: """Return the name of the current power source. @@ -516,6 +553,9 @@ class SonosSpeaker: self.battery_info = battery_info self.async_write_entity_states() + # + # Group management + # def update_groups(self, event: SonosEvent | None = None) -> None: """Handle callback for topology change event.""" coro = self.create_update_groups_coro(event) @@ -786,11 +826,9 @@ class SonosSpeaker: for speaker in hass.data[DATA_SONOS].discovered.values(): speaker.soco._zgs_cache.clear() # pylint: disable=protected-access - @property - def favorites(self) -> SonosFavorites: - """Return the SonosFavorites instance for this household.""" - return self.hass.data[DATA_SONOS].favorites[self.household_id] - + # + # Media and playback state handlers + # def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume @@ -951,11 +989,3 @@ class SonosSpeaker: elif update_media_position: self.media.position = current_position self.media.position_updated_at = dt_util.utcnow() - - @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None From 99afa15f476af4060958c9aef14a4c07da4bffd5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 May 2021 16:34:25 +0200 Subject: [PATCH 070/750] Cleanup unneeded variable assignment in ezviz (#51239) --- homeassistant/components/ezviz/binary_sensor.py | 1 - homeassistant/components/ezviz/sensor.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 9d8db7fbb30..abfe06d8daf 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -15,7 +15,6 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up Ezviz sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] sensors = [] - sensor_type_name = "None" for idx, camera in enumerate(coordinator.data): for name in camera: diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index f4f9f6588f0..fc07db89509 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -15,7 +15,6 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up Ezviz sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] sensors = [] - sensor_type_name = "None" for idx, camera in enumerate(coordinator.data): for name in camera: From 06e5314bc87d1a8607dd064770f802431c53aa41 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 May 2021 17:28:32 +0200 Subject: [PATCH 071/750] Cleanup commented code + comprehensions in iOS (#51238) --- homeassistant/components/ios/__init__.py | 28 +++++++++--------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 2feafba949d..6797da9d8a6 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -212,20 +212,20 @@ CONFIGURATION_FILE = ".ios.conf" def devices_with_push(hass): """Return a dictionary of push enabled targets.""" - targets = {} - for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items(): - if device.get(ATTR_PUSH_ID) is not None: - targets[device_name] = device.get(ATTR_PUSH_ID) - return targets + return { + device_name: device.get(ATTR_PUSH_ID) + for device_name, device in hass.data[DOMAIN][ATTR_DEVICES].items() + if device.get(ATTR_PUSH_ID) is not None + } def enabled_push_ids(hass): """Return a list of push enabled target push IDs.""" - push_ids = [] - for device in hass.data[DOMAIN][ATTR_DEVICES].values(): - if device.get(ATTR_PUSH_ID) is not None: - push_ids.append(device.get(ATTR_PUSH_ID)) - return push_ids + return [ + device.get(ATTR_PUSH_ID) + for device in hass.data[DOMAIN][ATTR_DEVICES].values() + if device.get(ATTR_PUSH_ID) is not None + ] def devices(hass): @@ -337,14 +337,6 @@ class iOSIdentifyDeviceView(HomeAssistantView): hass = request.app["hass"] - # Commented for now while iOS app is getting frequent updates - # try: - # data = IDENTIFY_SCHEMA(req_data) - # except vol.Invalid as ex: - # return self.json_message( - # vol.humanize.humanize_error(request.json, ex), - # HTTP_BAD_REQUEST) - data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() device_id = data[ATTR_DEVICE_ID] From ff6d05a2006a6ce25633a47bb313c9f670c6b1f9 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sat, 29 May 2021 18:50:45 +0200 Subject: [PATCH 072/750] Bump pyialarm to 1.7 (#51233) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 5cdc0ead3ea..e112a26003e 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.5"], + "requirements": ["pyialarm==1.7"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 4142f41a306..3caa2782327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1463,7 +1463,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb880c0ccd..940ab10cb87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -807,7 +807,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 From d1f0ec8db89113e2e4d71ac88357bd8a1a0aa4cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 May 2021 22:08:25 +0200 Subject: [PATCH 073/750] Small tweaks to LaCrosse (#51249) --- homeassistant/components/lacrosse/sensor.py | 47 ++++++--------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 32090797f11..7c5557757ef 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -68,7 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LaCrosse sensors.""" - usb_device = config.get(CONF_DEVICE) baud = int(config.get(CONF_BAUD)) expire_after = config.get(CONF_EXPIRE_AFTER) @@ -127,28 +126,22 @@ class LaCrosseSensor(SensorEntity): ENTITY_ID_FORMAT, device_id, hass=hass ) self._config = config - self._name = name self._value = None self._expire_after = expire_after self._expiration_trigger = None + self._attr_name = name lacrosse.register_callback( int(self._config["id"]), self._callback_lacrosse, None ) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def extra_state_attributes(self): """Return the state attributes.""" - attributes = { + return { "low_battery": self._low_battery, "new_battery": self._new_battery, } - return attributes def _callback_lacrosse(self, lacrosse_sensor, user_data): """Handle a function that is called from pylacrosse with new values.""" @@ -181,10 +174,7 @@ class LaCrosseSensor(SensorEntity): class LaCrosseTemperature(LaCrosseSensor): """Implementation of a Lacrosse temperature sensor.""" - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS + _attr_unit_of_measurement = TEMP_CELSIUS @property def state(self): @@ -195,21 +185,14 @@ class LaCrosseTemperature(LaCrosseSensor): class LaCrosseHumidity(LaCrosseSensor): """Implementation of a Lacrosse humidity sensor.""" - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE + _attr_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:water-percent" @property def state(self): """Return the state of the sensor.""" return self._humidity - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:water-percent" - class LaCrosseBattery(LaCrosseSensor): """Implementation of a Lacrosse battery sensor.""" @@ -218,23 +201,19 @@ class LaCrosseBattery(LaCrosseSensor): def state(self): """Return the state of the sensor.""" if self._low_battery is None: - state = None - elif self._low_battery is True: - state = "low" - else: - state = "ok" - return state + return None + if self._low_battery is True: + return "low" + return "ok" @property def icon(self): """Icon to use in the frontend.""" if self._low_battery is None: - icon = "mdi:battery-unknown" - elif self._low_battery is True: - icon = "mdi:battery-alert" - else: - icon = "mdi:battery" - return icon + return "mdi:battery-unknown" + if self._low_battery is True: + return "mdi:battery-alert" + return "mdi:battery" TYPE_CLASSES = { From ceadb0cba0f058428dbe2ef34d81ebd1b73ad29b Mon Sep 17 00:00:00 2001 From: astronaut Date: Sun, 30 May 2021 01:13:09 +0200 Subject: [PATCH 074/750] Add gui config option consider device unavailable (#51218) * Add gui config option consider device unavailable * Update tests --- homeassistant/components/zha/core/const.py | 13 +++++++++ homeassistant/components/zha/core/device.py | 23 ++++++++++++---- homeassistant/components/zha/core/gateway.py | 15 +++-------- homeassistant/components/zha/strings.json | 4 ++- .../components/zha/translations/en.json | 2 ++ tests/components/zha/test_device.py | 27 ++++++++++--------- tests/components/zha/test_device_trigger.py | 7 ++--- 7 files changed, 57 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index dcc2a080a76..ecb65981637 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -137,10 +137,23 @@ CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" CONF_ZIGPY = "zigpy_config" +CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" +CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours +CONF_CONSIDER_UNAVAILABLE_BATTERY = "consider_unavailable_battery" +CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours + CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_MAINS, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + ): cv.positive_int, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_BATTERY, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + ): cv.positive_int, } ) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 37608287609..0c572bfba8a 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -55,6 +55,10 @@ from .const import ( CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CONF_CONSIDER_UNAVAILABLE_BATTERY, + CONF_CONSIDER_UNAVAILABLE_MAINS, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, CONF_ENABLE_IDENTIFY_ON_JOIN, EFFECT_DEFAULT_VARIANT, EFFECT_OKAY, @@ -70,8 +74,6 @@ from .const import ( from .helpers import LogMixin, async_get_zha_config_value _LOGGER = logging.getLogger(__name__) -CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours -CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 @@ -107,9 +109,20 @@ class ZHADevice(LogMixin): ) if self.is_mains_powered: - self._consider_unavailable_time = CONSIDER_UNAVAILABLE_MAINS + self.consider_unavailable_time = async_get_zha_config_value( + self._zha_gateway.config_entry, + ZHA_OPTIONS, + CONF_CONSIDER_UNAVAILABLE_MAINS, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + ) else: - self._consider_unavailable_time = CONSIDER_UNAVAILABLE_BATTERY + self.consider_unavailable_time = async_get_zha_config_value( + self._zha_gateway.config_entry, + ZHA_OPTIONS, + CONF_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + ) + keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) self.unsubs.append( async_track_time_interval( @@ -320,7 +333,7 @@ class ZHADevice(LogMixin): return difference = time.time() - self.last_seen - if difference < self._consider_unavailable_time: + if difference < self.consider_unavailable_time: self.update_available(True) self._checkins_missed_count = 0 return diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4a9e6c28203..3ba5627cde8 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -77,12 +77,7 @@ from .const import ( ZHA_GW_MSG_RAW_INIT, RadioType, ) -from .device import ( - CONSIDER_UNAVAILABLE_BATTERY, - CONSIDER_UNAVAILABLE_MAINS, - DeviceStatus, - ZHADevice, -) +from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry @@ -185,17 +180,15 @@ class ZHAGateway: delta_msg = "not known" if zha_dev_entry and zha_dev_entry.last_seen is not None: delta = round(time.time() - zha_dev_entry.last_seen) - if zha_device.is_mains_powered: - zha_device.available = delta < CONSIDER_UNAVAILABLE_MAINS - else: - zha_device.available = delta < CONSIDER_UNAVAILABLE_BATTERY + zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( - "[%s](%s) restored as '%s', last seen: %s", + "[%s](%s) restored as '%s', last seen: %s, consider_unavailable_time: %s seconds", zha_device.nwk, zha_device.name, "available" if zha_device.available else "unavailable", delta_msg, + zha_device.consider_unavailable_time, ) # update the last seen time for devices every 10 minutes to avoid thrashing # writes and shutdown issues where storage isn't updated diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 9ca6a9821b3..9abff4e83e2 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -33,7 +33,9 @@ "zha_options": { "title": "Global Options", "enable_identify_on_join": "Enable identify effect when devices join the network", - "default_light_transition": "Default light transition time (seconds)" + "default_light_transition": "Default light transition time (seconds)", + "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", + "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)" }, "zha_alarm_options": { "title": "Alarm Control Panel Options", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 83dc56c75fc..cceb6cfdde6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -43,6 +43,8 @@ "zha_options": { "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", + "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", + "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "title": "Global Options" } }, diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index d3ab3c3ada2..d5ed0152b8b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -8,7 +8,10 @@ import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.general as general -import homeassistant.components.zha.core.device as zha_core_device +from homeassistant.components.zha.core.const import ( + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, +) from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE import homeassistant.helpers.device_registry as dr import homeassistant.util.dt as dt_util @@ -117,13 +120,13 @@ async def test_check_available_success( basic_ch.read_attributes.reset_mock() device_with_basic_channel.last_seen = None assert zha_device.available is True - _send_time_changed(hass, zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2) + _send_time_changed(hass, zha_device.consider_unavailable_time + 2) await hass.async_block_till_done() assert zha_device.available is False assert basic_ch.read_attributes.await_count == 0 device_with_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) _seens = [time.time(), device_with_basic_channel.last_seen] @@ -172,7 +175,7 @@ async def test_check_available_unsuccessful( assert basic_ch.read_attributes.await_count == 0 device_with_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) # unsuccessfuly ping zigpy device, but zha_device is still available @@ -213,7 +216,7 @@ async def test_check_available_no_basic_channel( assert zha_device.available is True device_without_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) assert "does not have a mandatory basic cluster" not in caplog.text @@ -246,38 +249,38 @@ async def test_ota_sw_version(hass, ota_zha_device): ("zigpy_device", 0, True), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, True, ), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, True, ), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, False, ), ("zigpy_device_mains", 0, True), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS - 2, True, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, False, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, False, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, False, ), ), diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index b2f964e2695..841d6b43400 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -7,7 +7,6 @@ import zigpy.profiles.zha import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation -import homeassistant.components.zha.core.device as zha_core_device from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -252,9 +251,7 @@ async def test_device_offline_fires( await hass.async_block_till_done() assert zha_device.available is True - zigpy_device.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2 - ) + zigpy_device.last_seen = time.time() - zha_device.consider_unavailable_time - 2 # there are 3 checkins to perform before marking the device unavailable future = dt_util.utcnow() + timedelta(seconds=90) @@ -266,7 +263,7 @@ async def test_device_offline_fires( await hass.async_block_till_done() future = dt_util.utcnow() + timedelta( - seconds=zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 100 + seconds=zha_device.consider_unavailable_time + 100 ) async_fire_time_changed(hass, future) await hass.async_block_till_done() From 2077efb207f67286b078db6aa7642652f678b89f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 30 May 2021 00:24:18 +0000 Subject: [PATCH 075/750] [ci skip] Translation update --- .../components/fritz/translations/it.json | 9 ++++ .../keenetic_ndms2/translations/it.json | 5 +- .../meteoclimatic/translations/it.json | 20 ++++++++ .../components/motioneye/translations/it.json | 4 ++ .../components/samsungtv/translations/nl.json | 4 +- .../components/sia/translations/it.json | 50 +++++++++++++++++++ .../totalconnect/translations/it.json | 5 +- .../components/wallbox/translations/it.json | 22 ++++++++ .../components/zha/translations/en.json | 4 +- 9 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/meteoclimatic/translations/it.json create mode 100644 homeassistant/components/sia/translations/it.json create mode 100644 homeassistant/components/wallbox/translations/it.json diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index f39b8bc7a7f..0169d275205 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -51,5 +51,14 @@ "title": "Configura gli strumenti del FRITZ!Box" } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Secondi per considerare un dispositivo \"a casa\"" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/it.json b/homeassistant/components/keenetic_ndms2/translations/it.json index e19961f5823..8ce00bbdd81 100644 --- a/homeassistant/components/keenetic_ndms2/translations/it.json +++ b/homeassistant/components/keenetic_ndms2/translations/it.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "no_udn": "Le informazioni di rilevamento SSDP non hanno UDN", + "not_keenetic_ndms2": "L'elemento rilevato non \u00e8 un router Keenetic" }, "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/meteoclimatic/translations/it.json b/homeassistant/components/meteoclimatic/translations/it.json new file mode 100644 index 00000000000..fcdfa25c496 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "unknown": "Errore imprevisto" + }, + "error": { + "not_found": "Nessun dispositivo trovato sulla rete" + }, + "step": { + "user": { + "data": { + "code": "Codice della stazione" + }, + "description": "Immettere il codice della stazione Meteoclimatic (ad esempio ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json index af07fac1a94..27e1167b3db 100644 --- a/homeassistant/components/motioneye/translations/it.json +++ b/homeassistant/components/motioneye/translations/it.json @@ -11,6 +11,10 @@ "unknown": "Errore imprevisto" }, "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant per connettersi al servizio motionEye fornito dal componente aggiuntivo: {addon}?", + "title": "motionEye tramite il componente aggiuntivo Home Assistant" + }, "user": { "data": { "admin_password": "Amministratore Password", diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 17a4fcb20ca..b4478994e1c 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", - "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.", + "auth_missing": "Home Assistant is niet gemachtigd om verbinding te maken met deze Samsung TV. Controleer de instellingen van Extern apparaatbeheer van uw tv om Home Assistant te machtigen.", "cannot_connect": "Kan geen verbinding maken", "id_missing": "Dit Samsung-apparaat heeft geen serienummer.", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund.", @@ -11,7 +11,7 @@ "unknown": "Onverwachte fout" }, "error": { - "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV." + "auth_missing": "Home Assistant is niet gemachtigd om verbinding te maken met deze Samsung TV. Controleer de instellingen van Extern apparaatbeheer van uw tv om Home Assistant te machtigen." }, "flow_title": "{device}", "step": { diff --git a/homeassistant/components/sia/translations/it.json b/homeassistant/components/sia/translations/it.json new file mode 100644 index 00000000000..7806cd0bed0 --- /dev/null +++ b/homeassistant/components/sia/translations/it.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "L'account non \u00e8 un valore esadecimale, utilizza solo 0-9 e AF.", + "invalid_account_length": "L'account non \u00e8 della lunghezza giusta, deve essere compreso tra 3 e 16 caratteri.", + "invalid_key_format": "La chiave non \u00e8 un valore esadecimale, utilizza solo 0-9 e AF.", + "invalid_key_length": "La chiave non \u00e8 della lunghezza corretta, deve essere di 16, 24 o 32 caratteri esadecimali.", + "invalid_ping": "L'intervallo di ping deve essere compreso tra 1 e 1440 minuti.", + "invalid_zones": "Deve essere presente almeno 1 zona.", + "unknown": "Errore imprevisto" + }, + "step": { + "additional_account": { + "data": { + "account": "ID account", + "additional_account": "Account aggiuntivi", + "encryption_key": "Chiave di crittografia", + "ping_interval": "Intervallo ping (min)", + "zones": "Numero di zone per l'account" + }, + "title": "Aggiungi un altro account alla porta corrente." + }, + "user": { + "data": { + "account": "ID account", + "additional_account": "Account aggiuntivi", + "encryption_key": "Chiave di crittografia", + "ping_interval": "Intervallo ping (min)", + "port": "Porta", + "protocol": "Protocollo", + "zones": "Numero di zone per l'account" + }, + "title": "Creare una connessione per i sistemi di allarme basati su SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignora il controllo del timestamp degli eventi SIA", + "zones": "Numero di zone per l'account" + }, + "description": "Imposta le opzioni per l'account: {account}", + "title": "Opzioni per l'impostazione SIA." + } + } + }, + "title": "Sistemi di allarme SIA" +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index 18ecf648310..dfc480ab961 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Posizione" + "location": "Posizione", + "usercode": "Codice utente" }, - "description": "Immettere il codice utente per questo utente in questa posizione", + "description": "Inserisci il codice utente per questo utente nella posizione {location_id}", "title": "Codici utente posizione" }, "reauth_confirm": { diff --git a/homeassistant/components/wallbox/translations/it.json b/homeassistant/components/wallbox/translations/it.json new file mode 100644 index 00000000000..5b8828860e7 --- /dev/null +++ b/homeassistant/components/wallbox/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "station": "Numero di serie della stazione", + "username": "Nome utente" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index cceb6cfdde6..e13aca2cfb1 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -41,10 +41,10 @@ "title": "Alarm Control Panel Options" }, "zha_options": { + "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", + "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", - "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", - "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "title": "Global Options" } }, From 3ca7eb9440f8b6b4b4825b8b550903a699b3de03 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 30 May 2021 11:41:23 +0800 Subject: [PATCH 076/750] Update HLS playlist in stream (#51191) * Enable gzip encoding for playlist responses * Add EXT-X-PROGRAM-DATE-TIME to playlist * Add EXT-X-START to playlist * Change EXT-X-VERSION from 7 to 6 * Move idle timer call to recv * Refactor recv to remove cursor and return bool * Rename STREAM_TIMEOUT to SOURCE_TIMEOUT --- homeassistant/components/stream/const.py | 12 ++- homeassistant/components/stream/core.py | 27 ++++--- homeassistant/components/stream/hls.py | 39 +++++++--- homeassistant/components/stream/worker.py | 4 +- tests/components/stream/test_hls.py | 89 ++++++++++++++++++++--- tests/components/stream/test_recorder.py | 4 +- 6 files changed, 131 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 20ff8210996..62d13321f91 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -21,15 +21,19 @@ NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist MAX_SEGMENTS = 4 # Max number of segments to keep around TARGET_SEGMENT_DURATION = 2.0 # Each segment is about this many seconds SEGMENT_DURATION_ADJUSTER = 0.1 # Used to avoid missing keyframe boundaries -MIN_SEGMENT_DURATION = ( - TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER -) # Each segment is at least this many seconds +# Each segment is at least this many seconds +MIN_SEGMENT_DURATION = TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER + +# Number of target durations to start before the end of the playlist. +# 1.5 should put us in the middle of the second to last segment even with +# variable keyframe intervals. +EXT_X_START = 1.5 PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable MAX_MISSING_DTS = 6 # Number of packets missing DTS to allow -STREAM_TIMEOUT = 30 # Timeout for reading stream +SOURCE_TIMEOUT = 30 # Timeout for reading stream source STREAM_RESTART_INCREMENT = 10 # Increase wait_timeout by this amount each retry STREAM_RESTART_RESET_TIME = 300 # Reset wait_timeout after this many seconds diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 76fae3cdacf..fcbc59ecdf3 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections import deque +import datetime from typing import Callable from aiohttp import web @@ -30,6 +31,7 @@ class Segment: duration: float = attr.ib() # For detecting discontinuities across stream restarts stream_id: int = attr.ib(default=0) + start_time: datetime.datetime = attr.ib(factory=datetime.datetime.utcnow) class IdleTimer: @@ -83,7 +85,6 @@ class StreamOutput: """Initialize a stream output.""" self._hass = hass self._idle_timer = idle_timer - self._cursor: int | None = None self._event = asyncio.Event() self._segments: deque[Segment] = deque(maxlen=deque_maxlen) @@ -109,6 +110,13 @@ class StreamOutput: """Return current sequence from segments.""" return [s.sequence for s in self._segments] + @property + def last_segment(self) -> Segment | None: + """Return the last segment without iterating.""" + if self._segments: + return self._segments[-1] + return None + @property def target_duration(self) -> int: """Return the max duration of any given segment in seconds.""" @@ -120,8 +128,6 @@ class StreamOutput: def get_segment(self, sequence: int) -> Segment | None: """Retrieve a specific segment.""" - self._idle_timer.awake() - for segment in self._segments: if segment.sequence == sequence: return segment @@ -129,20 +135,13 @@ class StreamOutput: def get_segments(self) -> deque[Segment]: """Retrieve all segments.""" - self._idle_timer.awake() return self._segments - async def recv(self) -> Segment | None: + async def recv(self) -> bool: """Wait for and retrieve the latest segment.""" - if self._cursor is None or self._cursor <= self.last_sequence: - await self._event.wait() - - if not self._segments: - return None - - segment = self.get_segments()[-1] - self._cursor = segment.sequence - return segment + self._idle_timer.awake() + await self._event.wait() + return self.last_segment is not None def put(self, segment: Segment) -> None: """Store output.""" diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 7b5185da6bf..be3598edb36 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -4,6 +4,7 @@ from aiohttp import web from homeassistant.core import callback from .const import ( + EXT_X_START, FORMAT_CONTENT_TYPE, HLS_PROVIDER, MAX_SEGMENTS, @@ -70,7 +71,7 @@ class HlsPlaylistView(StreamView): def render_preamble(track): """Render preamble.""" return [ - "#EXT-X-VERSION:7", + "#EXT-X-VERSION:6", f"#EXT-X-TARGETDURATION:{track.target_duration}", '#EXT-X-MAP:URI="init.mp4"', ] @@ -83,15 +84,31 @@ class HlsPlaylistView(StreamView): if not segments: return [] + first_segment = segments[0] playlist = [ - f"#EXT-X-MEDIA-SEQUENCE:{segments[0].sequence}", - f"#EXT-X-DISCONTINUITY-SEQUENCE:{segments[0].stream_id}", + f"#EXT-X-MEDIA-SEQUENCE:{first_segment.sequence}", + f"#EXT-X-DISCONTINUITY-SEQUENCE:{first_segment.stream_id}", + "#EXT-X-PROGRAM-DATE-TIME:" + + first_segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "Z", + # Since our window doesn't have many segments, we don't want to start + # at the beginning or we risk a behind live window exception in exoplayer. + # EXT-X-START is not supposed to be within 3 target durations of the end, + # but this seems ok + f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f},PRECISE=YES", ] - last_stream_id = segments[0].stream_id + last_stream_id = first_segment.stream_id for segment in segments: if last_stream_id != segment.stream_id: - playlist.append("#EXT-X-DISCONTINUITY") + playlist.extend( + [ + "#EXT-X-DISCONTINUITY", + "#EXT-X-PROGRAM-DATE-TIME:" + + segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "Z", + ] + ) playlist.extend( [ f"#EXTINF:{float(segment.duration):.04f},", @@ -115,7 +132,11 @@ class HlsPlaylistView(StreamView): if not track.sequences and not await track.recv(): return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} - return web.Response(body=self.render(track).encode("utf-8"), headers=headers) + response = web.Response( + body=self.render(track).encode("utf-8"), headers=headers + ) + response.enable_compression(web.ContentCoding.gzip) + return response class HlsInitView(StreamView): @@ -128,8 +149,7 @@ class HlsInitView(StreamView): async def handle(self, request, stream, sequence): """Return init.mp4.""" track = stream.add_provider(HLS_PROVIDER) - segments = track.get_segments() - if not segments: + if not (segments := track.get_segments()): return web.HTTPNotFound() headers = {"Content-Type": "video/mp4"} return web.Response(body=segments[0].init, headers=headers) @@ -145,8 +165,7 @@ class HlsSegmentView(StreamView): async def handle(self, request, stream, sequence): """Return fmp4 segment.""" track = stream.add_provider(HLS_PROVIDER) - segment = track.get_segment(int(sequence)) - if not segment: + if not (segment := track.get_segment(int(sequence))): return web.HTTPNotFound() headers = {"Content-Type": "video/iso.segment"} return web.Response( diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 6fe339c5dea..c606d1ad0dc 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -16,7 +16,7 @@ from .const import ( MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO, SEGMENT_CONTAINER_FORMAT, - STREAM_TIMEOUT, + SOURCE_TIMEOUT, ) from .core import Segment, StreamOutput from .fmp4utils import get_init_and_moof_data @@ -149,7 +149,7 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 """Handle consuming streams.""" try: - container = av.open(source, options=options, timeout=STREAM_TIMEOUT) + container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) except av.AVError: _LOGGER.error("Error opening stream %s", redact_credentials(str(source))) return diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 3e4b81bcc25..a31c686dcaf 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,5 +1,5 @@ """The tests for hls streams.""" -from datetime import timedelta +from datetime import datetime, timedelta from unittest.mock import patch from urllib.parse import urlparse @@ -23,9 +23,10 @@ from tests.components.stream.common import generate_h264_video STREAM_SOURCE = "some-stream-source" INIT_BYTES = b"init" MOOF_BYTES = b"some-bytes" -DURATION = 10 +SEGMENT_DURATION = 10 TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever +FAKE_TIME = datetime.utcnow() class HlsClient: @@ -61,7 +62,14 @@ def make_segment(segment, discontinuity=False): """Create a playlist response for a segment.""" response = [] if discontinuity: - response.append("#EXT-X-DISCONTINUITY") + response.extend( + [ + "#EXT-X-DISCONTINUITY", + "#EXT-X-PROGRAM-DATE-TIME:" + + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "Z", + ] + ) response.extend(["#EXTINF:10.0000,", f"./segment/{segment}.m4s"]), return "\n".join(response) @@ -70,11 +78,15 @@ def make_playlist(sequence, discontinuity_sequence=0, segments=[]): """Create a an hls playlist response for tests to assert on.""" response = [ "#EXTM3U", - "#EXT-X-VERSION:7", + "#EXT-X-VERSION:6", "#EXT-X-TARGETDURATION:10", '#EXT-X-MAP:URI="init.mp4"', f"#EXT-X-MEDIA-SEQUENCE:{sequence}", f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}", + "#EXT-X-PROGRAM-DATE-TIME:" + + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "Z", + f"#EXT-X-START:TIME-OFFSET=-{1.5*SEGMENT_DURATION:.3f},PRECISE=YES", ] response.extend(segments) response.append("") @@ -252,7 +264,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) - hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, SEGMENT_DURATION, start_time=FAKE_TIME)) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -261,7 +273,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): assert resp.status == 200 assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)]) - hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION)) + hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, SEGMENT_DURATION, start_time=FAKE_TIME)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 @@ -285,7 +297,15 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION)) + hls.put( + Segment( + sequence, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + start_time=FAKE_TIME, + ) + ) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") @@ -322,9 +342,36 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) - hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) - hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) - hls.put(Segment(3, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) + hls.put( + Segment( + 1, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + stream_id=0, + start_time=FAKE_TIME, + ) + ) + hls.put( + Segment( + 2, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + stream_id=0, + start_time=FAKE_TIME, + ) + ) + hls.put( + Segment( + 3, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + stream_id=1, + start_time=FAKE_TIME, + ) + ) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -354,11 +401,29 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy hls_client = await hls_stream(stream) - hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) + hls.put( + Segment( + 1, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + stream_id=0, + start_time=FAKE_TIME, + ) + ) # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) + hls.put( + Segment( + sequence, + INIT_BYTES, + MOOF_BYTES, + SEGMENT_DURATION, + stream_id=1, + start_time=FAKE_TIME, + ) + ) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 216e02a95b9..d45dd0cbca7 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -256,8 +256,8 @@ async def test_record_stream_audio( recorder = stream.add_provider(RECORDER_PROVIDER) while True: - segment = await recorder.recv() - if not segment: + await recorder.recv() + if not (segment := recorder.last_segment): break last_segment = segment stream_worker_sync.resume() From 32dc62a99601e86db4d867f6a75de9c4117ecca0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 May 2021 22:50:48 -0500 Subject: [PATCH 077/750] Handle empty ssdp descriptions in the cache (#51253) --- homeassistant/components/ssdp/descriptions.py | 2 +- tests/components/ssdp/test_init.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/descriptions.py b/homeassistant/components/ssdp/descriptions.py index a6fda3685f2..def302641ed 100644 --- a/homeassistant/components/ssdp/descriptions.py +++ b/homeassistant/components/ssdp/descriptions.py @@ -42,7 +42,7 @@ class DescriptionManager: @callback def async_cached_description(self, xml_location: str) -> None | dict[str, str]: """Fetch the description from the cache.""" - return self._description_cache[xml_location] + return self._description_cache.get(xml_location) async def _fetch_description(self, xml_location: str) -> None | dict[str, str]: """Fetch an XML description.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 6dfdc02f6e7..f5418bd227e 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -189,6 +189,7 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): aioclient_mock.get("http://1.1.1.1", exc=exc) mock_ssdp_response = { "st": "mock-st", + "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", "location": "http://1.1.1.1", } mock_get_ssdp = { @@ -203,6 +204,15 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc): assert not mock_init.mock_calls + assert ssdp.async_get_discovery_info_by_st(hass, "mock-st") == [ + { + "UDN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + "ssdp_location": "http://1.1.1.1", + "ssdp_st": "mock-st", + "ssdp_usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", + } + ] + async def test_scan_description_parse_fail(hass, aioclient_mock): """Test invalid XML.""" From c317854e866834f83aa4a5e5da92d29b3ae11eaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 May 2021 06:12:19 +0200 Subject: [PATCH 078/750] Small optimization in entity registry enabled deConz method (#51250) --- homeassistant/components/deconz/deconz_device.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index ab4d4083095..37a41845a00 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -58,15 +58,12 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.entities[self.TYPE].add(self.unique_id) @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry. Daylight is a virtual sensor from deCONZ that should never be enabled by default. """ - if self._device.type == "Daylight": - return False - - return True + return self._device.type != "Daylight" async def async_added_to_hass(self): """Subscribe to device events.""" From 416d91ba8565e77480f4d287e70d88ef1cf17b37 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 30 May 2021 01:10:49 -0700 Subject: [PATCH 079/750] Clean up SmartTub (#51257) * fix type hint * pylint * Update homeassistant/components/smarttub/binary_sensor.py Co-authored-by: Franck Nijhof * Update homeassistant/components/smarttub/binary_sensor.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/smarttub/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 7ab343d2015..76cbd21f946 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,4 +1,6 @@ """Platform for binary sensor integration.""" +from __future__ import annotations + import logging from smarttub import SpaError, SpaReminder @@ -144,7 +146,7 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): ) @property - def error(self) -> SpaError: + def error(self) -> SpaError | None: """Return the underlying SpaError object for this entity.""" errors = self.coordinator.data[self.spa.id][ATTR_ERRORS] if len(errors) == 0: From 0ae64325edca3340f91cd9c3c0a24f27689d4eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 30 May 2021 16:58:55 +0200 Subject: [PATCH 080/750] Use entity class vars for Mill (#51264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/climate.py | 48 +++++------------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 7a1adc6a0bc..45b628a772c 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -81,31 +81,26 @@ async def async_setup_entry(hass, entry, async_add_entities): class MillHeater(ClimateEntity): """Representation of a Mill Thermostat device.""" + _attr_fan_modes = [FAN_ON, HVAC_MODE_OFF] + _attr_max_temp = MAX_TEMP + _attr_min_temp = MIN_TEMP + _attr_supported_features = SUPPORT_FLAGS + _attr_target_temperature_step = 1 + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, heater, mill_data_connection): """Initialize the thermostat.""" self._heater = heater self._conn = mill_data_connection - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS + self._attr_unique_id = heater.device_id + self._attr_name = heater.name @property def available(self): """Return True if entity is available.""" return self._heater.available - @property - def unique_id(self): - """Return a unique ID.""" - return self._heater.device_id - - @property - def name(self): - """Return the name of the entity.""" - return self._heater.name - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -124,21 +119,11 @@ class MillHeater(ClimateEntity): res["room"] = "Independent device" return res - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS - @property def target_temperature(self): """Return the temperature we try to reach.""" return self._heater.set_temp - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 1 - @property def current_temperature(self): """Return the current temperature.""" @@ -149,21 +134,6 @@ class MillHeater(ClimateEntity): """Return the fan setting.""" return FAN_ON if self._heater.fan_status == 1 else HVAC_MODE_OFF - @property - def fan_modes(self): - """List of available fan modes.""" - return [FAN_ON, HVAC_MODE_OFF] - - @property - def min_temp(self): - """Return the minimum temperature.""" - return MIN_TEMP - - @property - def max_temp(self): - """Return the maximum temperature.""" - return MAX_TEMP - @property def hvac_action(self): """Return current hvac i.e. heat, cool, idle.""" From 319071ba39b25c97ef27a9bde71f42aa52d1739f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 31 May 2021 00:25:51 +0000 Subject: [PATCH 081/750] [ci skip] Translation update --- homeassistant/components/opentherm_gw/translations/pl.json | 3 ++- homeassistant/components/zha/translations/ca.json | 2 ++ homeassistant/components/zha/translations/nl.json | 2 ++ homeassistant/components/zha/translations/pl.json | 2 ++ homeassistant/components/zha/translations/ru.json | 2 ++ homeassistant/components/zha/translations/zh-Hant.json | 2 ++ 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index 9e1c4b1363d..69dd6040093 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -22,7 +22,8 @@ "data": { "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "read_precision": "Odczytaj precyzj\u0119", - "set_precision": "Ustaw precyzj\u0119" + "set_precision": "Ustaw precyzj\u0119", + "temporary_override_mode": "Tryb tymczasowej zmiany nastawy" }, "description": "Opcje dla bramki OpenTherm" } diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 0320ea34f2c..2467db76709 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -41,6 +41,8 @@ "title": "Opcions del panell de control d'alarma" }, "zha_options": { + "consider_unavailable_battery": "Considera els dispositius amb bateria com a no disponibles al cap de (segons)", + "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", "title": "Opcions globals" diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 86a3f7a7f69..f4cdb8f642b 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -41,6 +41,8 @@ "title": "Alarm bedieningspaneel Opties" }, "zha_options": { + "consider_unavailable_battery": "Overweeg apparaten met batterijvoeding als onbeschikbaar na (seconden)", + "consider_unavailable_mains": "Beschouw apparaten op netvoeding als onbeschikbaar na (seconden)", "default_light_transition": "Standaard licht transitietijd (seconden)", "enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen", "title": "Globale opties" diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index dce671771f3..37a708b100a 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -41,6 +41,8 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane z baterii niedost\u0119pne po (sekundach)", + "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z sieci niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "title": "Opcje og\u00f3lne" diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 291f5c0eea1..6f88ca16ae9 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -41,6 +41,8 @@ "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { + "consider_unavailable_battery": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "consider_unavailable_mains": "\u0412\u0440\u0435\u043c\u044f, \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u0431\u0443\u0434\u0435\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 43cc366ca35..d219e311791 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -41,6 +41,8 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", + "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "title": "Global \u9078\u9805" From 1a5d35d7bfff434bba6139e2559445ed77beef50 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 30 May 2021 20:28:22 -0500 Subject: [PATCH 082/750] Only debug log new Sonos SSDP discoveries (#51247) * Only debug log new SSDP discoveries * Use existing reference * Remove from known on unseen * Update homeassistant/components/sonos/speaker.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- homeassistant/components/sonos/__init__.py | 5 ++++- homeassistant/components/sonos/speaker.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 3b158a7db81..b7997a63571 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -78,6 +78,7 @@ class SonosData: self.alarms: dict[str, Alarm] = {} self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None + self.ssdp_known: set[str] = set() async def async_setup(hass, config): @@ -184,10 +185,12 @@ async def async_setup_entry( # noqa: C901 @callback def _async_discovered_player(info): - _LOGGER.debug("Sonos Discovery: %s", info) uid = info.get(ssdp.ATTR_UPNP_UDN) if uid.startswith("uuid:"): uid = uid[5:] + if uid not in data.ssdp_known: + _LOGGER.debug("New discovery: %s", info) + data.ssdp_known.add(uid) discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname asyncio.create_task(_async_create_discovered_player(uid, discovered_ip)) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 701fe5aa8c4..20a6aa794a1 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -460,6 +460,7 @@ class SonosSpeaker: await subscription.unsubscribe() self._subscriptions = [] + self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid) # # Alarm management From 9bd74961f0997238babf778b85ab57b0a58e8893 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 May 2021 05:55:45 +0200 Subject: [PATCH 083/750] Fix unnecessary API calls in Netatmo (#51260) --- homeassistant/components/netatmo/data_handler.py | 8 +++++++- homeassistant/components/netatmo/sensor.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1c092c40930..e93e602d6a7 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -158,7 +158,13 @@ class NetatmoDataHandler: ): """Register data class.""" if data_class_entry in self.data_classes: - self.data_classes[data_class_entry]["subscriptions"].append(update_callback) + if ( + update_callback + not in self.data_classes[data_class_entry]["subscriptions"] + ): + self.data_classes[data_class_entry]["subscriptions"].append( + update_callback + ) return self.data_classes[data_class_entry] = { diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index eeaab52a21a..ed75ddf2f7f 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -130,7 +130,7 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - platform_not_ready = False + platform_not_ready = True async def find_entities(data_class_name): """Find all entities.""" @@ -183,8 +183,8 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not data_class or not data_class.raw_data: - platform_not_ready = True + if not (data_class and data_class.raw_data): + platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -226,6 +226,12 @@ async def async_setup_entry(hass, entry, async_add_entities): lat_sw=area.lat_sw, lon_sw=area.lon_sw, ) + data_class = data_handler.data.get(signal_name) + + if not (data_class and data_class.raw_data): + nonlocal platform_not_ready + platform_not_ready = False + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: new_entities.append( NetatmoPublicSensor(data_handler, area, sensor_type) From e5309e89ea8d91f0b02ae3441b88abff34605a07 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 30 May 2021 23:03:53 -0500 Subject: [PATCH 084/750] Skip processed Sonos alarm updates (#51217) * Skip processed Sonos alarm updates * Fix bad conflict merge --- homeassistant/components/sonos/__init__.py | 3 ++- homeassistant/components/sonos/speaker.py | 10 ++++++++++ tests/components/sonos/conftest.py | 11 ++++++++++- tests/components/sonos/test_switch.py | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b7997a63571..d26d7d0c47a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections import OrderedDict +from collections import OrderedDict, deque import datetime import logging import socket @@ -76,6 +76,7 @@ class SonosData: self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, Alarm] = {} + self.processed_alarm_events = deque(maxlen=5) self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None self.ssdp_known: set[str] = set() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 20a6aa794a1..0d7efae1877 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import deque from collections.abc import Coroutine import contextlib import datetime @@ -282,6 +283,11 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None + @property + def processed_alarm_events(self) -> deque[str]: + """Return the container of processed alarm events.""" + return self.hass.data[DATA_SONOS].processed_alarm_events + @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -366,6 +372,10 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" + update_id = event.variables["alarm_list_version"] + if update_id in self.processed_alarm_events: + return + self.processed_alarm_events.append(update_id) self.hass.async_add_executor_job(self.update_alarms) @callback diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 81ad0e6c8ef..7c5b4ac91ef 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -31,6 +31,15 @@ class SonosMockEvent: self.service = service self.variables = variables + def increment_variable(self, var_name): + """Increment the value of the var_name key in variables dict attribute. + + Assumes value has a format of :. + """ + base, count = self.variables[var_name].split(":") + newcount = int(count) + 1 + self.variables[var_name] = ":".join([base, str(newcount)]) + @pytest.fixture(name="config_entry") def config_entry_fixture(): @@ -174,7 +183,7 @@ def alarm_event_fixture(soco): "time_zone": "ffc40a000503000003000502ffc4", "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", "time_generation": "20000001", - "alarm_list_version": "RINCON_test", + "alarm_list_version": "RINCON_test:1", "time_format": "INV", "date_format": "INV", "daily_index_refresh_time": None, diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d4448d22b32..41cb241d377 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -68,6 +68,7 @@ async def test_alarm_create_delete( assert "switch.sonos_alarm_15" in entity_registry.entities alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + alarm_event.increment_variable("alarm_list_version") sub_callback(event=alarm_event) await hass.async_block_till_done() From 7654672dd078144da387dcf29b1ea24e9e11a528 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 May 2021 21:04:13 -0700 Subject: [PATCH 085/750] Updated frontend to 20210531.0 (#51281) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9ee97c851e9..61da986b06b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210528.0" + "home-assistant-frontend==20210531.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cfc0585fc84..920357148e1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3caa2782327..63c328f4c1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 940ab10cb87..1088370db43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210528.0 +home-assistant-frontend==20210531.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a8650f4e59121364990d5b0c6a204a7ceac95131 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 31 May 2021 02:46:28 -0400 Subject: [PATCH 086/750] Add zwave_js node status sensor (#51181) * Add zwave_js node status sensor * fix import * use parent class name property * Use more entity class attributes * Update homeassistant/components/zwave_js/sensor.py Co-authored-by: Franck Nijhof * return static values in property method * fix PR * switch to class atributes * create sensor platform task if needed Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/__init__.py | 14 ++++ homeassistant/components/zwave_js/sensor.py | 73 +++++++++++++++++++ tests/components/zwave_js/test_init.py | 4 +- tests/components/zwave_js/test_sensor.py | 46 ++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index beebe2cc3f8..3d5d7ab7601 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -15,6 +15,7 @@ from zwave_js_server.model.notification import ( ) from zwave_js_server.model.value import Value, ValueNotification +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -178,6 +179,19 @@ async def async_setup_entry( # noqa: C901 if disc_info.assumed_state: value_updates_disc_info.append(disc_info) + # We need to set up the sensor platform if it hasn't already been setup in + # order to create the node status sensor + if SENSOR_DOMAIN not in platform_setup_tasks: + platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) + ) + await platform_setup_tasks[SENSOR_DOMAIN] + + # Create a node status sensor for each device + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node + ) + # add listener for value updated events if necessary if value_updates_disc_info: unsubscribe_callbacks.append( diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 40e28999a1a..b3c7db25116 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -6,6 +6,7 @@ from typing import cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, ConfigurationValueType +from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ConfigurationValue from homeassistant.components.sensor import ( @@ -31,6 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .helpers import get_device_id LOGGER = logging.getLogger(__name__) @@ -66,6 +68,11 @@ async def async_setup_entry( async_add_entities(entities) + @callback + def async_add_node_status_sensor(node: ZwaveNode) -> None: + """Add node status sensor.""" + async_add_entities([ZWaveNodeStatusSensor(config_entry, client, node)]) + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( async_dispatcher_connect( hass, @@ -74,6 +81,14 @@ async def async_setup_entry( ) ) + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_node_status_sensor", + async_add_node_status_sensor, + ) + ) + class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" @@ -295,3 +310,61 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): return None # add the value's int value as property for multi-value (list) items return {"value": self.info.primary_value.value} + + +class ZWaveNodeStatusSensor(SensorEntity): + """Representation of a node status sensor.""" + + _attr_should_poll = False + _attr_entity_registry_enabled_default = False + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, node: ZwaveNode + ) -> None: + """Initialize a generic Z-Wave device entity.""" + self.config_entry = config_entry + self.client = client + self.node = node + name: str = ( + self.node.name + or self.node.device_config.description + or f"Node {self.node.node_id}" + ) + # Entity class attributes + self._attr_name = f"{name}: Node Status" + self._attr_unique_id = ( + f"{self.client.driver.controller.home_id}.{node.node_id}.node_status" + ) + # device is precreated in main handler + self._attr_device_info = { + "identifiers": {get_device_id(self.client, self.node)}, + } + self._attr_state: str = node.status.name.lower() + + async def async_poll_value(self, _: bool) -> None: + """Poll a value.""" + raise ValueError("There is no value to poll for this entity") + + def _status_changed(self, _: dict) -> None: + """Call when status event is received.""" + self._attr_state = self.node.status.name.lower() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + # Add value_changed callbacks. + for evt in ("wake up", "sleep", "dead", "alive"): + self.async_on_remove(self.node.on(evt, self._status_changed)) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_poll_value", + self.async_poll_value, + ) + ) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return self.client.connected and bool(self.node.ready) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 67d5a416a91..840d1b15b4d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -916,7 +916,7 @@ async def test_removed_device(hass, client, multiple_devices, integration): # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 24 + assert len(entity_entries) == 26 # Remove a node and reload the entry old_node = nodes.pop(13) @@ -928,7 +928,7 @@ async def test_removed_device(hass, client, multiple_devices, integration): device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 15 + assert len(entity_entries) == 16 assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index afd3ae1a984..fc6d274235d 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,4 +1,6 @@ """Test the Z-Wave JS sensor platform.""" +from zwave_js_server.event import Event + from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -85,3 +87,47 @@ async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration) entity_entry = ent_reg.async_get(ID_LOCK_CONFIG_PARAMETER_SENSOR) assert entity_entry assert entity_entry.disabled + + +async def test_node_status_sensor(hass, lock_id_lock_as_id150, integration): + """Test node status sensor is created and gets updated on node state changes.""" + NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status" + node = lock_id_lock_as_id150 + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(NODE_STATUS_ENTITY) + assert entity_entry.disabled + assert entity_entry.disabled_by == er.DISABLED_INTEGRATION + updated_entry = ent_reg.async_update_entity( + entity_entry.entity_id, **{"disabled_by": None} + ) + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + assert not updated_entry.disabled + assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" + + # Test transitions work + event = Event( + "dead", data={"source": "node", "event": "dead", "nodeId": node.node_id} + ) + node.receive_event(event) + assert hass.states.get(NODE_STATUS_ENTITY).state == "dead" + + event = Event( + "wake up", data={"source": "node", "event": "wake up", "nodeId": node.node_id} + ) + node.receive_event(event) + assert hass.states.get(NODE_STATUS_ENTITY).state == "awake" + + event = Event( + "sleep", data={"source": "node", "event": "sleep", "nodeId": node.node_id} + ) + node.receive_event(event) + assert hass.states.get(NODE_STATUS_ENTITY).state == "asleep" + + event = Event( + "alive", data={"source": "node", "event": "alive", "nodeId": node.node_id} + ) + node.receive_event(event) + assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" From 489c73b4da3089548e25b6a12a3de76427f01c23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 May 2021 09:47:15 +0200 Subject: [PATCH 087/750] Simplify device action code (#51263) --- .../alarm_control_panel/device_action.py | 51 +++----------- .../components/climate/device_action.py | 24 +++---- .../components/cover/device_action.py | 69 ++++--------------- homeassistant/components/fan/device_action.py | 22 ++---- .../components/humidifier/device_action.py | 23 ++----- .../components/light/device_action.py | 40 +++++------ .../components/lock/device_action.py | 33 +++------ .../components/vacuum/device_action.py | 24 +++---- .../components/water_heater/device_action.py | 24 +++---- .../integration/device_action.py | 24 +++---- 10 files changed, 95 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 506552f8a50..bb2188807bb 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -70,51 +70,22 @@ async def async_get_actions( supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + # Add actions for each entity that belongs to this integration if supported_features & SUPPORT_ALARM_ARM_AWAY: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_away", - } - ) + actions.append({**base_action, CONF_TYPE: "arm_away"}) if supported_features & SUPPORT_ALARM_ARM_HOME: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_home", - } - ) + actions.append({**base_action, CONF_TYPE: "arm_home"}) if supported_features & SUPPORT_ALARM_ARM_NIGHT: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_night", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "disarm", - } - ) + actions.append({**base_action, CONF_TYPE: "arm_night"}) + actions.append({**base_action, CONF_TYPE: "disarm"}) if supported_features & SUPPORT_ALARM_TRIGGER: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "trigger", - } - ) + actions.append({**base_action, CONF_TYPE: "trigger"}) return actions diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 02474a47f96..de46eae0d7b 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -54,23 +54,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if state is None: continue - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_hvac_mode", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.append({**base_action, CONF_TYPE: "set_hvac_mode"}) if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_preset_mode", - } - ) + actions.append({**base_action, CONF_TYPE: "set_preset_mode"}) return actions diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 74eef8102df..24c8a0c8b3b 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -75,72 +75,29 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] # Add actions for each entity that belongs to this integration + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + if supported_features & SUPPORT_SET_POSITION: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_position", - } - ) + actions.append({**base_action, CONF_TYPE: "set_position"}) else: if supported_features & SUPPORT_OPEN: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "open", - } - ) + actions.append({**base_action, CONF_TYPE: "open"}) if supported_features & SUPPORT_CLOSE: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "close", - } - ) + actions.append({**base_action, CONF_TYPE: "close"}) if supported_features & SUPPORT_STOP: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "stop", - } - ) + actions.append({**base_action, CONF_TYPE: "stop"}) if supported_features & SUPPORT_SET_TILT_POSITION: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_tilt_position", - } - ) + actions.append({**base_action, CONF_TYPE: "set_tilt_position"}) else: if supported_features & SUPPORT_OPEN_TILT: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "open_tilt", - } - ) + actions.append({**base_action, CONF_TYPE: "open_tilt"}) if supported_features & SUPPORT_CLOSE_TILT: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "close_tilt", - } - ) + actions.append({**base_action, CONF_TYPE: "close_tilt"}) return actions diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index f4611d353d5..ddf6a76d3c8 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -38,22 +38,12 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_on", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_off", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + actions += [{**base_action, CONF_TYPE: action} for action in ACTION_TYPES] return actions diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index fa9c1eb71e7..7e11b65fd2e 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -52,28 +52,19 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: state = hass.states.get(entry.entity_id) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_humidity", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + actions.append({**base_action, CONF_TYPE: "set_humidity"}) # We need a state or else we can't populate the available modes. if state is None: continue if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES: - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_mode", - } - ) + actions.append({**base_action, CONF_TYPE: "set_mode"}) return actions diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 3de2218d7c7..f533cc75586 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -11,7 +11,14 @@ from homeassistant.components.light import ( VALID_BRIGHTNESS_PCT, VALID_FLASH, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_DOMAIN, CONF_TYPE, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + SERVICE_TURN_ON, +) from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_supported_features @@ -111,35 +118,22 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: supported_color_modes = get_supported_color_modes(hass, entry.entity_id) supported_features = get_supported_features(hass, entry.entity_id) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + if brightness_supported(supported_color_modes): actions.extend( ( - { - CONF_TYPE: TYPE_BRIGHTNESS_INCREASE, - "device_id": device_id, - "entity_id": entry.entity_id, - "domain": DOMAIN, - }, - { - CONF_TYPE: TYPE_BRIGHTNESS_DECREASE, - "device_id": device_id, - "entity_id": entry.entity_id, - "domain": DOMAIN, - }, + {**base_action, CONF_TYPE: TYPE_BRIGHTNESS_INCREASE}, + {**base_action, CONF_TYPE: TYPE_BRIGHTNESS_DECREASE}, ) ) if supported_features & SUPPORT_FLASH: - actions.extend( - ( - { - CONF_TYPE: TYPE_FLASH, - "device_id": device_id, - "entity_id": entry.entity_id, - "domain": DOMAIN, - }, - ) - ) + actions.append({**base_action, CONF_TYPE: TYPE_FLASH}) return actions diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index cb0e2b0daad..06a133465aa 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -41,35 +41,20 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: continue # Add actions for each entity that belongs to this integration - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "lock", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "unlock", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.append({**base_action, CONF_TYPE: "lock"}) + actions.append({**base_action, CONF_TYPE: "unlock"}) state = hass.states.get(entry.entity_id) if state: features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & (SUPPORT_OPEN): - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "open", - } - ) + actions.append({**base_action, CONF_TYPE: "open"}) return actions diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index 2308882469e..a4df68c3b93 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -36,22 +36,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "clean", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "dock", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.append({**base_action, CONF_TYPE: "clean"}) + actions.append({**base_action, CONF_TYPE: "dock"}) return actions diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py index e1c84be8753..3662dee9a5e 100644 --- a/homeassistant/components/water_heater/device_action.py +++ b/homeassistant/components/water_heater/device_action.py @@ -37,22 +37,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_on", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_off", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.append({**base_action, CONF_TYPE: "turn_on"}) + actions.append({**base_action, CONF_TYPE: "turn_off"}) return actions diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index 3bd1c0b91b3..720e472851c 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -48,22 +48,14 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: # Add actions for each entity that belongs to this integration # TODO add your own actions. - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_on", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turn_off", - } - ) + base_action = { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + actions.append({**base_action, CONF_TYPE: "turn_on"}) + actions.append({**base_action, CONF_TYPE: "turn_off"}) return actions From 04e9acc20abb248dc23d062fee917735dfe23b74 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 May 2021 09:47:30 +0200 Subject: [PATCH 088/750] Simplify device condition code (#51266) --- .../alarm_control_panel/device_condition.py | 61 ++++------------- .../components/climate/device_condition.py | 27 +++----- .../components/cover/device_condition.py | 66 ++++--------------- .../device_tracker/device_condition.py | 26 +++----- .../components/fan/device_condition.py | 26 +++----- .../components/lock/device_condition.py | 26 +++----- .../media_player/device_condition.py | 53 +++------------ .../components/vacuum/device_condition.py | 26 +++----- .../integration/device_condition.py | 26 +++----- 9 files changed, 82 insertions(+), 255 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index fa4f903f2e5..db010051010 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -79,61 +79,26 @@ async def async_get_conditions( supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] # Add conditions for each entity that belongs to this integration + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + conditions += [ - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_DISARMED, - }, - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_TRIGGERED, - }, + {**base_condition, CONF_TYPE: CONDITION_DISARMED}, + {**base_condition, CONF_TYPE: CONDITION_TRIGGERED}, ] if supported_features & SUPPORT_ALARM_ARM_HOME: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_ARMED_HOME, - } - ) + conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME}) if supported_features & SUPPORT_ALARM_ARM_AWAY: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_ARMED_AWAY, - } - ) + conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY}) if supported_features & SUPPORT_ALARM_ARM_NIGHT: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_ARMED_NIGHT, - } - ) + conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT}) if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS: conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS, - } + {**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS} ) return conditions diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index d20c202e93b..3792f1219a4 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -54,29 +54,20 @@ async def async_get_conditions( state = hass.states.get(entry.entity_id) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_hvac_mode", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions.append({**base_condition, CONF_TYPE: "is_hvac_mode"}) if ( state and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE ): - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_preset_mode", - } - ) + conditions.append({**base_condition, CONF_TYPE: "is_preset_mode"}) return conditions diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 2943f589f7b..148e2fb48b0 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -85,63 +85,21 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) # Add conditions for each entity that belongs to this integration + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + if supports_open_close: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_open", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closed", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_opening", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closing", - } - ) + conditions += [ + {**base_condition, CONF_TYPE: cond} for cond in STATE_CONDITION_TYPES + ] if supported_features & SUPPORT_SET_POSITION: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_position", - } - ) + conditions.append({**base_condition, CONF_TYPE: "is_position"}) if supported_features & SUPPORT_SET_TILT_POSITION: - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_tilt_position", - } - ) + conditions.append({**base_condition, CONF_TYPE: "is_tilt_position"}) return conditions diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 0260a4bbd3a..714d6d7f016 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -43,24 +43,14 @@ async def async_get_conditions( continue # Add conditions for each entity that belongs to this integration - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_home", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_not_home", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 9aa9620ef72..56d9208b2d2 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -42,24 +42,14 @@ async def async_get_conditions( if entry.domain != DOMAIN: continue - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_on", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_off", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 0fae680f829..3e77a23ffdb 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -41,24 +41,14 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict continue # Add conditions for each entity that belongs to this integration - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_locked", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_unlocked", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 0e6e0f96c40..e392c274f33 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -46,51 +46,14 @@ async def async_get_conditions( continue # Add conditions for each entity that belongs to this integration - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_on", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_off", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_idle", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_paused", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_playing", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 4803ebdb988..a66df1323f7 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -40,24 +40,14 @@ async def async_get_conditions( if entry.domain != DOMAIN: continue - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_cleaning", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_docked", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 413812828e5..4180f81b3ff 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -45,24 +45,14 @@ async def async_get_conditions( # Add conditions for each entity that belongs to this integration # TODO add your own conditions. - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_on", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_off", - } - ) + base_condition = { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + + conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] return conditions From 532626b7383b276577f70356f93ddee5aecc3a39 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 31 May 2021 09:47:58 +0200 Subject: [PATCH 089/750] Move light helper get_supported_color_modes (#51269) --- homeassistant/components/light/__init__.py | 24 +++++++++++++++++-- .../components/light/device_action.py | 21 +--------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 27f3bbfc0c6..7c650c06cf8 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -18,8 +18,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, HomeAssistantError, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -116,6 +116,26 @@ def color_temp_supported(color_modes: Iterable[str] | None) -> bool: return COLOR_MODE_COLOR_TEMP in color_modes +def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None: + """Get supported color modes for a light entity. + + First try the statemachine, then entity registry. + This is the equivalent of entity helper get_supported_features. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + if not entry.capabilities: + return None + + return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES) + + # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index f533cc75586..2180bdd3094 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -27,9 +27,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ( ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, - ATTR_SUPPORTED_COLOR_MODES, DOMAIN, brightness_supported, + get_supported_color_modes, ) TYPE_BRIGHTNESS_INCREASE = "brightness_increase" @@ -50,25 +50,6 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None: - """Get supported color modes for a light entity. - - First try the statemachine, then entity registry. - """ - state = hass.states.get(entity_id) - if state: - return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - - entity_registry = er.async_get(hass) - entry = entity_registry.async_get(entity_id) - if not entry: - raise HomeAssistantError(f"Unknown entity {entity_id}") - if not entry.capabilities: - return None - - return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES) - - async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, From 5acc3a1083a5b73378f584c6ba95ff36c1d8f6d5 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 31 May 2021 09:58:48 +0200 Subject: [PATCH 090/750] xknx 0.18.3 (#51277) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0a722d46162..b1ed504f7ad 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.2"], + "requirements": ["xknx==0.18.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 63c328f4c1a..9b48631dc1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2374,7 +2374,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.2 +xknx==0.18.3 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1088370db43..3acf168d339 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1280,7 +1280,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.2 +xknx==0.18.3 # homeassistant.components.bluesound # homeassistant.components.rest From 258b388f4104908ebba2074eb62695705712c244 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 May 2021 10:50:11 +0200 Subject: [PATCH 091/750] Collection of changing entity properties to class attributes (#51248) * Collection of changing entity properties to class attributes * Apply suggestions from code review Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/advantage_air/binary_sensor.py | 21 ++---- .../components/advantage_air/sensor.py | 21 ++---- homeassistant/components/aten_pe/switch.py | 7 +- homeassistant/components/co2signal/sensor.py | 13 +--- .../components/deconz/binary_sensor.py | 7 +- homeassistant/components/deconz/climate.py | 6 +- homeassistant/components/delijn/sensor.py | 7 +- homeassistant/components/emonitor/sensor.py | 13 +--- .../homekit_controller/binary_sensor.py | 36 +++------- .../components/homekit_controller/sensor.py | 65 +++++-------------- homeassistant/components/homematic/cover.py | 7 +- .../components/ign_sismologia/geo_location.py | 7 +- .../keenetic_ndms2/binary_sensor.py | 13 +--- .../components/lutron/binary_sensor.py | 7 +- homeassistant/components/nmbs/sensor.py | 7 +- homeassistant/components/shelly/cover.py | 7 +- .../components/sighthound/image_processing.py | 7 +- .../components/smart_meter_texas/sensor.py | 7 +- .../components/smarttub/binary_sensor.py | 21 ++---- homeassistant/components/soma/sensor.py | 11 +--- homeassistant/components/somfy/sensor.py | 13 +--- .../components/syncthru/binary_sensor.py | 14 ++-- homeassistant/components/tradfri/sensor.py | 13 +--- .../components/trafikverket_train/sensor.py | 7 +- homeassistant/components/upcloud/__init__.py | 7 +- .../components/vultr/binary_sensor.py | 7 +- homeassistant/components/wsdot/sensor.py | 14 ++-- homeassistant/components/xbee/__init__.py | 7 +- homeassistant/components/xbee/sensor.py | 7 +- .../components/xiaomi_miio/sensor.py | 13 +--- .../components/xiaomi_miio/switch.py | 7 +- homeassistant/components/zha/cover.py | 12 +--- 32 files changed, 106 insertions(+), 305 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index f7b295c9634..fba90148788 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -33,6 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" + _attr_device_class = DEVICE_CLASS_PROBLEM + @property def name(self): """Return the name.""" @@ -43,11 +45,6 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): """Return a unique id.""" return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-filter' - @property - def device_class(self): - """Return the device class of the vent.""" - return DEVICE_CLASS_PROBLEM - @property def is_on(self): """Return if filter needs cleaning.""" @@ -57,6 +54,8 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone Motion.""" + _attr_device_class = DEVICE_CLASS_MOTION + @property def name(self): """Return the name.""" @@ -67,11 +66,6 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): """Return a unique id.""" return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-motion' - @property - def device_class(self): - """Return the device class of the vent.""" - return DEVICE_CLASS_MOTION - @property def is_on(self): """Return if motion is detect.""" @@ -81,6 +75,8 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" + _attr_entity_registry_enabled_default = False + @property def name(self): """Return the name.""" @@ -95,8 +91,3 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): def is_on(self): """Return if this zone is the myZone.""" return self._zone["number"] == self._ac["myZone"] - - @property - def entity_registry_enabled_default(self): - """Return false to disable this entity by default.""" - return False diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8f027b1bdaf..8c6834ac76e 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -44,6 +44,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" + _attr_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" super().__init__(instance, ac_key) @@ -65,11 +67,6 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Return the current value.""" return self._ac[self._time_key] - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ADVANTAGE_AIR_SET_COUNTDOWN_UNIT - @property def icon(self): """Return a representative icon of the timer.""" @@ -86,6 +83,8 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" + _attr_unit_of_measurement = PERCENTAGE + @property def name(self): """Return the name.""" @@ -103,11 +102,6 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): return self._zone["value"] return 0 - @property - def unit_of_measurement(self): - """Return the percent sign.""" - return PERCENTAGE - @property def icon(self): """Return a representative icon.""" @@ -119,6 +113,8 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" + _attr_unit_of_measurement = PERCENTAGE + @property def name(self): """Return the name.""" @@ -134,11 +130,6 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Return the current value of the wireless signal.""" return self._zone["rssi"] - @property - def unit_of_measurement(self): - """Return the percent sign.""" - return PERCENTAGE - @property def icon(self): """Return a representative icon.""" diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 1bf54085064..43146938961 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -67,6 +67,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class AtenSwitch(SwitchEntity): """Represents an ATEN PE switch.""" + _attr_device_class = DEVICE_CLASS_OUTLET + def __init__(self, device, mac, outlet, name): """Initialize an ATEN PE switch.""" self._device = device @@ -86,11 +88,6 @@ class AtenSwitch(SwitchEntity): """Return the name of the entity.""" return self._name - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_OUTLET - @property def is_on(self) -> bool: """Return True if entity is on.""" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index c7d2a64d6b0..980ffa8549b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -54,6 +54,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class CO2Sensor(SensorEntity): """Implementation of the CO2Signal sensor.""" + _attr_icon = "mdi:molecule-co2" + _attr_unit_of_measurement = CO2_INTENSITY_UNIT + def __init__(self, token, country_code, lat, lon): """Initialize the sensor.""" self._token = token @@ -74,21 +77,11 @@ class CO2Sensor(SensorEntity): """Return the name of the sensor.""" return self._friendly_name - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return "mdi:molecule-co2" - @property def state(self): """Return the state of the device.""" return self._data - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return CO2_INTENSITY_UNIT - @property def extra_state_attributes(self): """Return the state attributes of the last update.""" diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index de23d06e7db..b19301aa113 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -127,6 +127,8 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN + _attr_device_class = DEVICE_CLASS_PROBLEM + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" @@ -148,8 +150,3 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): def name(self) -> str: """Return the name of the sensor.""" return f"{self._device.name} Tampered" - - @property - def device_class(self) -> str: - """Return the class of the sensor.""" - return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1ef881e9c90..db68843080a 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -110,6 +110,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" TYPE = DOMAIN + _attr_temperature_unit = TEMP_CELSIUS def __init__(self, device, gateway): """Set up thermostat device.""" @@ -238,11 +239,6 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): await self._device.async_set_config(data) - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def extra_state_attributes(self): """Return the state attributes of the thermostat.""" diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index cff93c89954..b105ff5ff7b 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -60,6 +60,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class DeLijnPublicTransportSensor(SensorEntity): """Representation of a Ruter sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, line): """Initialize the sensor.""" self.line = line @@ -104,11 +106,6 @@ class DeLijnPublicTransportSensor(SensorEntity): """Return True if entity is available.""" return self._available - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 2b9d715ba51..1dca3f2d89d 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -37,6 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Representation of an Emonitor power sensor entity.""" + _attr_device_class = DEVICE_CLASS_POWER + _attr_unit_of_measurement = POWER_WATT + def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: """Initialize the channel sensor.""" self.channel_number = channel_number @@ -62,16 +65,6 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): """Name of the sensor.""" return self.channel_data.label - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return POWER_WATT - - @property - def device_class(self) -> str: - """Device class of the sensor.""" - return DEVICE_CLASS_POWER - def _paired_attr(self, attr_name: str) -> float: """Cumulative attributes for channel and paired channel.""" attr_val = getattr(self.channel_data, attr_name) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 537e9c2a698..64257c47f47 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -19,15 +19,12 @@ from . import KNOWN_DEVICES, HomeKitEntity class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit motion sensor.""" + _attr_device_class = DEVICE_CLASS_MOTION + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.MOTION_DETECTED] - @property - def device_class(self): - """Define this binary_sensor as a motion sensor.""" - return DEVICE_CLASS_MOTION - @property def is_on(self): """Has motion been detected.""" @@ -37,15 +34,12 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit contact sensor.""" + _attr_device_class = DEVICE_CLASS_OPENING + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.CONTACT_STATE] - @property - def device_class(self): - """Define this binary_sensor as a opening sensor.""" - return DEVICE_CLASS_OPENING - @property def is_on(self): """Return true if the binary sensor is on/open.""" @@ -55,10 +49,7 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit smoke sensor.""" - @property - def device_class(self) -> str: - """Return the class of this sensor.""" - return DEVICE_CLASS_SMOKE + _attr_device_class = DEVICE_CLASS_SMOKE def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -73,10 +64,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit BO sensor.""" - @property - def device_class(self) -> str: - """Return the class of this sensor.""" - return DEVICE_CLASS_GAS + _attr_device_class = DEVICE_CLASS_GAS def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -91,10 +79,7 @@ class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit occupancy sensor.""" - @property - def device_class(self) -> str: - """Return the class of this sensor.""" - return DEVICE_CLASS_OCCUPANCY + _attr_device_class = DEVICE_CLASS_OCCUPANCY def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" @@ -109,15 +94,12 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): """Representation of a Homekit leak sensor.""" + _attr_device_class = DEVICE_CLASS_MOISTURE + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.LEAK_DETECTED] - @property - def device_class(self): - """Define this binary_sensor as a leak sensor.""" - return DEVICE_CLASS_MOISTURE - @property def is_on(self): """Return true if a leak is detected from the binary sensor.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 2ae264fabb9..0aa8d24da38 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -43,15 +43,13 @@ SIMPLE_SENSOR = { class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_unit_of_measurement = PERCENTAGE + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_HUMIDITY - @property def name(self): """Return the name of the device.""" @@ -62,11 +60,6 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Return the sensor icon.""" return HUMIDITY_ICON - @property - def unit_of_measurement(self): - """Return units for the sensor.""" - return PERCENTAGE - @property def state(self): """Return the current humidity.""" @@ -76,15 +69,13 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit temperature sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.TEMPERATURE_CURRENT] - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_TEMPERATURE - @property def name(self): """Return the name of the device.""" @@ -95,11 +86,6 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Return the sensor icon.""" return TEMP_C_ICON - @property - def unit_of_measurement(self): - """Return units for the sensor.""" - return TEMP_CELSIUS - @property def state(self): """Return the current temperature in Celsius.""" @@ -109,15 +95,13 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit light level sensor.""" + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_unit_of_measurement = LIGHT_LUX + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_ILLUMINANCE - @property def name(self): """Return the name of the device.""" @@ -128,11 +112,6 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Return the sensor icon.""" return BRIGHTNESS_ICON - @property - def unit_of_measurement(self): - """Return units for the sensor.""" - return LIGHT_LUX - @property def state(self): """Return the current light level in lux.""" @@ -142,6 +121,9 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit Carbon Dioxide sensor.""" + _attr_icon = CO2_ICON + _attr_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] @@ -151,16 +133,6 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} CO2" - @property - def icon(self): - """Return the sensor icon.""" - return CO2_ICON - - @property - def unit_of_measurement(self): - """Return units for the sensor.""" - return CONCENTRATION_PARTS_PER_MILLION - @property def state(self): """Return the current CO2 level in ppm.""" @@ -170,6 +142,9 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): class HomeKitBatterySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit battery sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [ @@ -178,11 +153,6 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): CharacteristicsTypes.CHARGING_STATE, ] - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY - @property def name(self): """Return the name of the device.""" @@ -210,11 +180,6 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): return icon - @property - def unit_of_measurement(self): - """Return units for the sensor.""" - return PERCENTAGE - @property def is_low_battery(self): """Return true if battery level is low.""" diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index e9f2943b53b..deed671931f 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -112,6 +112,8 @@ class HMCover(HMDevice, CoverEntity): class HMGarage(HMCover): """Represents a Homematic Garage cover. Homematic garage covers do not support position attributes.""" + _attr_device_class = DEVICE_CLASS_GARAGE + @property def current_cover_position(self): """ @@ -127,11 +129,6 @@ class HMGarage(HMCover): """Return whether the cover is closed.""" return self._hmdevice.is_closed(self._hm_get_state()) - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_GARAGE - def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" self._state = "DOOR_STATE" diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 314a7bdea31..2ef9cbf4eeb 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -132,6 +132,8 @@ class IgnSismologiaFeedEntityManager: class IgnSismologiaLocationEvent(GeolocationEvent): """This represents an external event with IGN Sismologia feed data.""" + _attr_unit_of_measurement = LENGTH_KILOMETERS + def __init__(self, feed_manager, external_id): """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager @@ -233,11 +235,6 @@ class IgnSismologiaLocationEvent(GeolocationEvent): """Return longitude value of this external event.""" return self._longitude - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - @property def extra_state_attributes(self): """Return the device state attributes.""" diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index ed366bd7402..e8f7df02489 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -27,6 +27,9 @@ async def async_setup_entry( class RouterOnlineBinarySensor(BinarySensorEntity): """Representation router connection status.""" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_should_poll = False + def __init__(self, router: KeeneticRouter) -> None: """Initialize the APCUPSd binary device.""" self._router = router @@ -46,16 +49,6 @@ class RouterOnlineBinarySensor(BinarySensorEntity): """Return true if the UPS is online, else false.""" return self._router.available - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_CONNECTIVITY - - @property - def should_poll(self) -> bool: - """Return False since entity pushes its state to HA.""" - return False - @property def device_info(self): """Return a client description for device registry.""" diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index 6fb394d333c..db5aa5dcccc 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -29,17 +29,14 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity): reported as a single occupancy group. """ + _attr_device_class = DEVICE_CLASS_OCCUPANCY + @property def is_on(self): """Return true if the binary sensor is on.""" # Error cases will end up treated as unoccupied. return self._lutron_device.state == OccupancyGroup.State.OCCUPIED - @property - def device_class(self): - """Return that this is an occupancy sensor.""" - return DEVICE_CLASS_OCCUPANCY - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 58ad547eaec..26f7dbd2c8a 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -166,6 +166,8 @@ class NMBSLiveBoard(SensorEntity): class NMBSSensor(SensorEntity): """Get the the total travel time for a given connection.""" + _attr_unit_of_measurement = TIME_MINUTES + def __init__( self, api_client, name, show_on_map, station_from, station_to, excl_vias ): @@ -185,11 +187,6 @@ class NMBSSensor(SensorEntity): """Return the name of the sensor.""" return self._name - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TIME_MINUTES - @property def icon(self): """Return the sensor default icon or an alert icon if any delay.""" diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 18f13479c30..dc2dba654f3 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -31,6 +31,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ShellyCover(ShellyBlockEntity, CoverEntity): """Switch that controls a cover block on Shelly devices.""" + _attr_device_class = DEVICE_CLASS_SHUTTER + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize light.""" super().__init__(wrapper, block) @@ -76,11 +78,6 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): """Flag supported features.""" return self._supported_features - @property - def device_class(self) -> str: - """Return the class of the device.""" - return DEVICE_CLASS_SHUTTER - async def async_close_cover(self, **kwargs): """Close cover.""" self.control_result = await self.set_state(go="close") diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index fa636eb757f..e31b30f1174 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -75,6 +75,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SighthoundEntity(ImageProcessingEntity): """Create a sighthound entity.""" + _attr_unit_of_measurement = ATTR_PEOPLE + def __init__( self, api, camera_entity, name, save_file_folder, save_timestamped_file ): @@ -164,11 +166,6 @@ class SighthoundEntity(ImageProcessingEntity): """Return the state of the entity.""" return self._state - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ATTR_PEOPLE - @property def extra_state_attributes(self): """Return the attributes.""" diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index 13e93fe362b..f63edcce0fc 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -33,6 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -40,11 +42,6 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self._state = None self._available = False - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ENERGY_KILO_WATT_HOUR - @property def name(self): """Device Name.""" diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 76cbd21f946..331c0b7e3d7 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -63,6 +63,8 @@ async def async_setup_entry(hass, entry, async_add_entities): class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): """A binary sensor indicating whether the spa is currently online (connected to the cloud).""" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__(coordinator, spa, "Online", "online") @@ -80,15 +82,12 @@ class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state is True - @property - def device_class(self) -> str: - """Return the device class for this entity.""" - return DEVICE_CLASS_CONNECTIVITY - class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Reminders for maintenance actions.""" + _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, coordinator, spa, reminder): """Initialize the entity.""" super().__init__( @@ -120,11 +119,6 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): ATTR_REMINDER_SNOOZED: self.reminder.snoozed, } - @property - def device_class(self) -> str: - """Return the device class for this entity.""" - return DEVICE_CLASS_PROBLEM - async def async_snooze(self, days): """Snooze this reminder for the specified number of days.""" await self.reminder.snooze(days) @@ -137,6 +131,8 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): There may be 0 or more errors. If there are >0, we show the first one. """ + _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__( @@ -175,8 +171,3 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): ATTR_CREATED_AT: error.created_at.isoformat(), ATTR_UPDATED_AT: error.updated_at.isoformat(), } - - @property - def device_class(self) -> str: - """Return the device class for this entity.""" - return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 436a92a1087..4df12c9f8f5 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -29,10 +29,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomaSensor(SomaEntity, SensorEntity): """Representation of a Soma cover device.""" - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_BATTERY + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE @property def name(self): @@ -44,11 +42,6 @@ class SomaSensor(SomaEntity, SensorEntity): """Return the state of the entity.""" return self.battery_state - @property - def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return PERCENTAGE - @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update the sensor with the latest data.""" diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 34283a1271c..312c425cf87 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -30,6 +30,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): """Representation of a Somfy thermostat battery.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, coordinator, device_id, api): """Initialize the Somfy device.""" super().__init__(coordinator, device_id, api) @@ -44,13 +47,3 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): def state(self) -> int: """Return the state of the sensor.""" return self._climate.get_battery() - - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of the sensor.""" - return PERCENTAGE diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 66bf76b31a5..7c4bd6fa8d1 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -75,16 +75,13 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): class SyncThruOnlineSensor(SyncThruBinarySensor): """Implementation of a sensor that checks whether is turned on/online.""" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + def __init__(self, syncthru, name): """Initialize the sensor.""" super().__init__(syncthru, name) self._id_suffix = "_online" - @property - def device_class(self): - """Class of the sensor.""" - return DEVICE_CLASS_CONNECTIVITY - @property def is_on(self): """Set the state to whether the printer is online.""" @@ -94,16 +91,13 @@ class SyncThruOnlineSensor(SyncThruBinarySensor): class SyncThruProblemSensor(SyncThruBinarySensor): """Implementation of a sensor that checks whether the printer works correctly.""" + _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, syncthru, name): """Initialize the sensor.""" super().__init__(syncthru, name) self._id_suffix = "_problem" - @property - def device_class(self): - """Class of the sensor.""" - return DEVICE_CLASS_PROBLEM - @property def is_on(self): """Set the state to whether there is a problem with the printer.""" diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 455ca69147d..1f028849d32 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -29,22 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TradfriSensor(TradfriBaseDevice, SensorEntity): """The platform class required by Home Assistant.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, device, api, gateway_id): """Initialize the device.""" super().__init__(device, api, gateway_id) self._unique_id = f"{gateway_id}-{device.id}" - @property - def device_class(self): - """Return the devices' state attributes.""" - return DEVICE_CLASS_BATTERY - @property def state(self): """Return the current state of the device.""" return self._device.device_info.battery_level - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return PERCENTAGE diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 37e3bd52cdc..5e541045266 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -118,6 +118,8 @@ def next_departuredate(departure): class TrainSensor(SensorEntity): """Contains data about a train depature.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, train_api, name, from_station, to_station, weekday, time): """Initialize the sensor.""" self._train_api = train_api @@ -176,11 +178,6 @@ class TrainSensor(SensorEntity): ATTR_DEVIATIONS: deviations, } - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 57267c92cf7..925d41a3252 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -240,6 +240,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class UpCloudServerEntity(CoordinatorEntity): """Entity class for UpCloud servers.""" + _attr_device_class = DEFAULT_COMPONENT_DEVICE_CLASS + def __init__( self, coordinator: DataUpdateCoordinator[dict[str, upcloud_api.Server]], @@ -284,11 +286,6 @@ class UpCloudServerEntity(CoordinatorEntity): """Return true if the server is on.""" return self.state == STATE_ON - @property - def device_class(self) -> str: - """Return the class of this server.""" - return DEFAULT_COMPONENT_DEVICE_CLASS - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the UpCloud server.""" diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index c62d5136aa6..f5f03c62872 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -54,6 +54,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class VultrBinarySensor(BinarySensorEntity): """Representation of a Vultr subscription sensor.""" + _attr_device_class = DEFAULT_DEVICE_CLASS + def __init__(self, vultr, subscription, name): """Initialize a new Vultr binary sensor.""" self._vultr = vultr @@ -80,11 +82,6 @@ class VultrBinarySensor(BinarySensorEntity): """Return true if the binary sensor is on.""" return self.data["power_status"] == "running" - @property - def device_class(self): - """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS - @property def extra_state_attributes(self): """Return the state attributes of the Vultr subscription.""" diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 9e4d957d028..8b45326cdbd 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -73,6 +73,8 @@ class WashingtonStateTransportSensor(SensorEntity): can read them and make them available. """ + _attr_icon = ICON + def __init__(self, name, access_code): """Initialize the sensor.""" self._data = {} @@ -90,15 +92,12 @@ class WashingtonStateTransportSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" + _attr_unit_of_measurement = TIME_MINUTES + def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" self._travel_time_id = travel_time_id @@ -135,11 +134,6 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): ) return attrs - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return TIME_MINUTES - def _parse_wsdot_timestamp(timestamp): """Convert WSDOT timestamp to datetime.""" diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 58ce7587070..13cd4217b4d 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -369,6 +369,8 @@ class XBeeDigitalOut(XBeeDigitalIn): class XBeeAnalogIn(SensorEntity): """Representation of a GPIO pin configured as an analog input.""" + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, config, device): """Initialize the XBee analog in device.""" self._config = config @@ -418,11 +420,6 @@ class XBeeAnalogIn(SensorEntity): """Return the state of the entity.""" return self._value - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return PERCENTAGE - def update(self): """Get the latest reading from the ADC.""" try: diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 78cfe964277..18e4b0c7aa1 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -46,6 +46,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" + _attr_unit_of_measurement = TEMP_CELSIUS + def __init__(self, config, device): """Initialize the sensor.""" self._config = config @@ -62,11 +64,6 @@ class XBeeTemperatureSensor(SensorEntity): """Return the state of the sensor.""" return self._temp - @property - def unit_of_measurement(self): - """Return the unit of measurement the value is expressed in.""" - return TEMP_CELSIUS - def update(self): """Get the latest data.""" try: diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 16ca4d3e7ec..5d271a772b9 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -273,6 +273,9 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): class XiaomiGatewayIlluminanceSensor(SensorEntity): """Representation of the gateway device's illuminance sensor.""" + _attr_device_class = DEVICE_CLASS_ILLUMINANCE + _attr_unit_of_measurement = UNIT_LUMEN + def __init__(self, gateway_device, gateway_name, gateway_device_id): """Initialize the entity.""" self._gateway = gateway_device @@ -302,16 +305,6 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): """Return true when state is known.""" return self._available - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return UNIT_LUMEN - - @property - def device_class(self): - """Return the device class of this entity.""" - return DEVICE_CLASS_ILLUMINANCE - @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 09a9786c372..ace0e52eaea 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -262,6 +262,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Representation of a XiaomiGatewaySwitch.""" + _attr_device_class = DEVICE_CLASS_SWITCH + def __init__(self, coordinator, sub_device, entry, variable): """Initialize the XiaomiSensor.""" super().__init__(coordinator, sub_device, entry) @@ -270,11 +272,6 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): self._unique_id = f"{sub_device.sid}-ch{self._channel}" self._name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})" - @property - def device_class(self): - """Return the device class of this entity.""" - return DEVICE_CLASS_SWITCH - @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 35080c56921..71c5dcca908 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -177,6 +177,8 @@ class ZhaCover(ZhaEntity, CoverEntity): class Shade(ZhaEntity, CoverEntity): """ZHA Shade.""" + _attr_device_class = DEVICE_CLASS_SHADE + def __init__( self, unique_id: str, @@ -199,11 +201,6 @@ class Shade(ZhaEntity, CoverEntity): """ return self._position - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_SHADE - @property def is_closed(self) -> bool | None: """Return True if shade is closed.""" @@ -289,10 +286,7 @@ class Shade(ZhaEntity, CoverEntity): class KeenVent(Shade): """Keen vent cover.""" - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_DAMPER + _attr_device_class = DEVICE_CLASS_DAMPER async def async_open_cover(self, **kwargs): """Open the cover.""" From 3d119fd4abe66bec43c9b4746dca42d77ca88ad8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 31 May 2021 14:03:26 +0200 Subject: [PATCH 092/750] Revert "GRPC is fixed, don't need a workaround" (#51289) This reverts commit 9d174e8a0504a83831530ef9c9178bbbe5a58872. --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 920357148e1..0ae8841b186 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -47,6 +47,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 79d4c05b0b6..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,10 @@ h11>=0.12.0 # https://github.com/advisories/GHSA-93xj-8mrv-444m httplib2>=0.19.0 +# gRPC 1.32+ currently causes issues on ARMv7, see: +# https://github.com/home-assistant/core/issues/40148 +grpcio==1.31.0 + # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From 8ed8747225a5516b09dab2feccf7625073de7f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 31 May 2021 14:06:11 +0200 Subject: [PATCH 093/750] Resolve addon repository slug for device registry (#51287) * Resolve addon repository slug for device registry * typo * Adjust onboarding test * Use /store --- homeassistant/components/hassio/__init__.py | 28 +++++++++++++++++++-- homeassistant/components/hassio/handler.py | 8 ++++++ tests/components/hassio/test_init.py | 23 +++++++++++------ tests/components/onboarding/test_views.py | 3 +++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e33c689c59e..d391817f964 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -71,6 +71,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_CORE_INFO = "hassio_core_info" DATA_HOST_INFO = "hassio_host_info" +DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" @@ -291,6 +292,16 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_store(hass): + """Return store information. + + Async friendly. + """ + return hass.data.get(DATA_STORE) + + @callback @bind_hass def get_supervisor_info(hass): @@ -456,6 +467,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: try: hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() + hass.data[DATA_STORE] = await hassio.get_store() hass.data[DATA_CORE_INFO] = await hassio.get_core_info() hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() hass.data[DATA_OS_INFO] = await hassio.get_os_info() @@ -627,10 +639,22 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" new_data = {} - addon_data = get_supervisor_info(self.hass) + supervisor_info = get_supervisor_info(self.hass) + store_data = get_store(self.hass) + + repositories = { + repo[ATTR_SLUG]: repo[ATTR_NAME] + for repo in store_data.get("repositories", []) + } new_data["addons"] = { - addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", []) + addon[ATTR_SLUG]: { + **addon, + ATTR_REPOSITORY: repositories.get( + addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") + ), + } + for addon in supervisor_info.get("addons", []) } if self.is_hass_os: new_data["os"] = get_os_info(self.hass) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 301d353faf0..37b645eb7d3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -118,6 +118,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_store(self): + """Return data from the store. + + This method return a coroutine. + """ + return self.send_command("/store", method="get") + @api_data def get_ingress_panels(self): """Return data for Add-on ingress panels. diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 5bf8a45ab52..7e9d7cd91c8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -32,6 +32,13 @@ def mock_all(aioclient_mock, request): "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, }, ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) aioclient_mock.get( "http://127.0.0.1/host/info", json={ @@ -67,6 +74,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com/home-assistant/addons/test", }, { @@ -76,6 +84,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com", }, ], @@ -92,7 +101,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -131,7 +140,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -147,7 +156,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -159,7 +168,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -206,7 +215,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -220,7 +229,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -237,7 +246,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index d8ae50b851f..a921dfe39d4 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -73,6 +73,9 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_store", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_supervisor_info", return_value={"diagnostics": True}, From 5377e5ed38e09d23d79196b6eb1862fa40642a0e Mon Sep 17 00:00:00 2001 From: Salvatore Mazzarino Date: Mon, 31 May 2021 14:45:56 +0200 Subject: [PATCH 094/750] Update to pygtfs 0.1.6 (#51267) * Update to pygtfs 0.1.6 Signed-off-by: Salvatore Mazzarino * run tasks: generate requirements --- homeassistant/components/gtfs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index d987899463f..4de42e3190a 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -2,7 +2,7 @@ "domain": "gtfs", "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", - "requirements": ["pygtfs==0.1.5"], + "requirements": ["pygtfs==0.1.6"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9b48631dc1f..e6c1045a8f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gtfs -pygtfs==0.1.5 +pygtfs==0.1.6 # homeassistant.components.hvv_departures pygti==0.9.2 From edcae7433036f32d1f8beef6194e701d503a5b6e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 May 2021 14:54:42 +0200 Subject: [PATCH 095/750] Entity attributes + typing fix in deCONZ alarm control panel (#51241) --- .../components/deconz/alarm_control_panel.py | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 6bb4b72e89d..4fc3e2ad0b8 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -102,35 +102,20 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): TYPE = DOMAIN + _attr_code_arm_required = False + _attr_supported_features = ( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ) + def __init__(self, device, gateway) -> None: """Set up alarm control panel device.""" super().__init__(device, gateway) - - self._features = SUPPORT_ALARM_ARM_AWAY - self._features |= SUPPORT_ALARM_ARM_HOME - self._features |= SUPPORT_ALARM_ARM_NIGHT - self._service_to_device_panel_command = { PANEL_ENTRY_DELAY: self._device.entry_delay, PANEL_EXIT_DELAY: self._device.exit_delay, PANEL_NOT_READY_TO_ARM: self._device.not_ready_to_arm, } - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return self._features - - @property - def code_arm_required(self) -> bool: - """Code is not required for arm actions.""" - return False - - @property - def code_format(self) -> None: - """Code is not supported.""" - return None - @callback def async_update_callback(self, force_update: bool = False) -> None: """Update the control panels state.""" @@ -142,7 +127,7 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): super().async_update_callback(force_update=force_update) @property - def state(self) -> str: + def state(self) -> str | None: """Return the state of the control panel.""" return DECONZ_TO_ALARM_STATE.get(self._device.state) From c9178e58b5231a00643700c86032e7ce94bba71c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 31 May 2021 16:00:58 +0200 Subject: [PATCH 096/750] Add support for state class for Airly sensor (#51285) --- homeassistant/components/airly/const.py | 5 +++++ homeassistant/components/airly/model.py | 1 + homeassistant/components/airly/sensor.py | 3 ++- tests/components/airly/test_sensor.py | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index c3dd51e4f69..a860a7a1b5a 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Final +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -50,23 +51,27 @@ SENSOR_TYPES: dict[str, SensorDescription] = { ATTR_ICON: "mdi:blur", ATTR_LABEL: ATTR_API_PM1, ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_API_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), ATTR_UNIT: PERCENTAGE, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_API_PRESSURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ATTR_ICON: None, ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), ATTR_UNIT: PRESSURE_HPA, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_API_TEMPERATURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_ICON: None, ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), ATTR_UNIT: TEMP_CELSIUS, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, } diff --git a/homeassistant/components/airly/model.py b/homeassistant/components/airly/model.py index 42091d449e3..6109b6e71d9 100644 --- a/homeassistant/components/airly/model.py +++ b/homeassistant/components/airly/model.py @@ -11,3 +11,4 @@ class SensorDescription(TypedDict): icon: str | None label: str unit: str + state_class: str diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 4544a806349..562cfec10d5 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -66,6 +66,7 @@ class AirlySensor(CoordinatorEntity, SensorEntity): self._state = None self._unit_of_measurement = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_state_class = self._description[ATTR_STATE_CLASS] @property def name(self) -> str: diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 925f3acb6d2..7db3c81f44d 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta from homeassistant.components.airly.sensor import ATTRIBUTION +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -38,6 +39,7 @@ async def test_sensor(hass, aioclient_mock): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_humidity") assert entry @@ -52,6 +54,7 @@ async def test_sensor(hass, aioclient_mock): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pm1") assert entry @@ -63,6 +66,7 @@ async def test_sensor(hass, aioclient_mock): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_pressure") assert entry @@ -74,6 +78,7 @@ async def test_sensor(hass, aioclient_mock): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_temperature") assert entry From 52e7d5753321a06ce72932dfce3a19669cd267aa Mon Sep 17 00:00:00 2001 From: Nikolai Date: Mon, 31 May 2021 17:35:49 +0300 Subject: [PATCH 097/750] Processing of messages from channel by telegram_bot (#51274) * Processing of messages from channel by telegram_bot * formatted using Black * refactor * check allowed chat --- .../components/telegram_bot/__init__.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index fe3728ba91b..4b8e661b572 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -68,6 +68,7 @@ ATTR_USERNAME = "username" ATTR_VERIFY_SSL = "verify_ssl" ATTR_TIMEOUT = "timeout" ATTR_MESSAGE_TAG = "message_tag" +ATTR_CHANNEL_POST = "channel_post" CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids" CONF_PROXY_URL = "proxy_url" @@ -866,6 +867,31 @@ class BaseTelegramBotEntity: return True, data + def _get_channel_post_data(self, msg_data): + """Return boolean msg_data_is_ok and dict msg_data.""" + if not msg_data: + return False, None + + if "sender_chat" in msg_data and "chat" in msg_data and "text" in msg_data: + if ( + msg_data["sender_chat"].get("id") not in self.allowed_chat_ids + and msg_data["chat"].get("id") not in self.allowed_chat_ids + ): + # Neither sender_chat id nor chat id was in allowed_chat_ids, + # origin is not allowed. + _LOGGER.error("Incoming message is not allowed (%s)", msg_data) + return True, None + + data = { + ATTR_MSGID: msg_data["message_id"], + ATTR_CHAT_ID: msg_data["chat"]["id"], + ATTR_TEXT: msg_data["text"], + } + return True, data + + _LOGGER.error("Incoming message does not have required data (%s)", msg_data) + return False, None + def process_message(self, data): """Check for basic message rules and fire an event if message is ok.""" if ATTR_MSG in data or ATTR_EDITED_MSG in data: @@ -916,6 +942,15 @@ class BaseTelegramBotEntity: self.hass.bus.async_fire(event, event_data) return True + if ATTR_CHANNEL_POST in data: + event = EVENT_TELEGRAM_TEXT + data = data.get(ATTR_CHANNEL_POST) + message_ok, event_data = self._get_channel_post_data(data) + if event_data is None: + return message_ok + + self.hass.bus.async_fire(event, event_data) + return True _LOGGER.warning("Message with unknown data received: %s", data) return True From d2623bf5749e06b0019d50779fbc36f4ec65761e Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 31 May 2021 13:59:55 -0400 Subject: [PATCH 098/750] AppleTV typo in error notification (#51300) An extraneous "f" was prefix at the beginning of the notification. >An irrecoverable connection problem occurred when connecting to fApple TV. Please go to the Integrations page and reconfigure it. --- homeassistant/components/apple_tv/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index a1bd50ab221..e8b900d8213 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -270,7 +270,7 @@ class AppleTVManager: self.hass.components.persistent_notification.create( "An irrecoverable connection problem occurred when connecting to " - f"`f{name}`. Please go to the Integrations page and reconfigure it", + f"`{name}`. Please go to the Integrations page and reconfigure it", title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, ) From 5a4add64378806e818fe0b5c18fe6ef53bf9d9e8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 May 2021 20:04:20 +0200 Subject: [PATCH 099/750] Upgrade black to 21.5b2 (#51297) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ead1fd09bb..8106b2c074f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.5b1 + rev: 21.5b2 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e403e05fcfd..a6d8f3f9b04 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.5b1 +black==21.5b2 codespell==2.0.0 flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 From 0e7c2cddf7fe6fc904681a15c7546bc8fb714736 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 May 2021 13:47:12 -0500 Subject: [PATCH 100/750] Upgrade HAP-python to 3.5.0 (#51261) * Upgrade HAP-python to 3.4.2 - Fixes for malformed event sending - Performance improvements * Bump * update tests to point to async --- homeassistant/components/homekit/__init__.py | 8 ++++---- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/conftest.py | 2 +- tests/components/homekit/test_homekit.py | 17 ++++++++++------- tests/components/homekit/test_type_cameras.py | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 32b528f567c..3f297892446 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -456,7 +456,7 @@ class HomeKit: self.bridge = None self.driver = None - def setup(self, zeroconf_instance): + def setup(self, async_zeroconf_instance): """Set up bridge and accessory driver.""" ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -471,7 +471,7 @@ class HomeKit: port=self._port, persist_file=persist_file, advertised_address=self._advertise_ip, - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) # If we do not load the mac address will be wrong @@ -595,8 +595,8 @@ class HomeKit: if self.status != STATUS_READY: return self.status = STATUS_WAIT - zc_instance = await zeroconf.async_get_instance(self.hass) - await self.hass.async_add_executor_job(self.setup, zc_instance) + async_zc_instance = await zeroconf.async_get_async_instance(self.hass) + await self.hass.async_add_executor_job(self.setup, async_zc_instance) self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) await self.aid_storage.async_initialize() await self._async_create_accessories() diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 483279d55f3..d2c2f094a0f 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.4.1", + "HAP-python==3.5.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index e6c1045a8f3..d6d871d3126 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3acf168d339..9961342391d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 469a0a7deb7..5441bcc195c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -12,7 +12,7 @@ from tests.common import async_capture_events @pytest.fixture def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index fd7d74aeaba..bd7af3b3596 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -250,7 +250,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=zeroconf_mock, + async_zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False @@ -290,7 +290,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=mock_zeroconf, + async_zeroconf_instance=mock_zeroconf, ) @@ -315,10 +315,10 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): entry_title=entry.title, ) - zeroconf_instance = MagicMock() + async_zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, @@ -329,7 +329,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) @@ -851,7 +851,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): options={}, ) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) - system_zc = await zeroconf.async_get_instance(hass) + system_async_zc = await zeroconf.async_get_async_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" @@ -859,7 +859,10 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc + assert ( + hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser + == system_async_zc + ) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index ba08ea3caaf..354db900470 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -80,7 +80,7 @@ async def _async_stop_stream(hass, acc, session_info): @pytest.fixture() def run_driver(hass): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer"), patch( "pyhap.accessory_driver.AccessoryDriver.publish" From 7403ba1e81579b4ab83da24e570d4afe864e6312 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 31 May 2021 20:58:01 +0200 Subject: [PATCH 101/750] Alexa fan preset_mode support (#50466) * fan preset_modes * process preset mode updates from alexa correctly * add tests * codecov patch additional tests --- .../components/alexa/capabilities.py | 19 ++++- homeassistant/components/alexa/entities.py | 5 ++ homeassistant/components/alexa/handlers.py | 10 +++ tests/components/alexa/test_capabilities.py | 42 +++++++++ tests/components/alexa/test_smart_home.py | 85 ++++++++++++++++++- tests/components/alexa/test_state_report.py | 34 +++++++- 6 files changed, 189 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 483b4484261..10b382c8dcf 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1155,8 +1155,6 @@ class AlexaPowerLevelController(AlexaCapability): if self.entity.domain == fan.DOMAIN: return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 - return None - class AlexaSecurityPanelController(AlexaCapability): """Implements Alexa.SecurityPanelController. @@ -1304,6 +1302,12 @@ class AlexaModeController(AlexaCapability): if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN): return f"{fan.ATTR_DIRECTION}.{mode}" + # Fan preset_mode + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": + mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None) + if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None): + return f"{fan.ATTR_PRESET_MODE}.{mode}" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": # Return state instead of position when using ModeController. @@ -1342,6 +1346,17 @@ class AlexaModeController(AlexaCapability): ) return self._resource.serialize_capability_resources() + # Fan preset_mode + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": + self._resource = AlexaModeResource( + [AlexaGlobalCatalog.SETTING_PRESET], False + ) + for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []): + self._resource.add_mode( + f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode] + ) + return self._resource.serialize_capability_resources() + # Cover Position Resources if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": self._resource = AlexaModeResource( diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 723d115b923..cef18623bf5 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -535,6 +535,7 @@ class FanCapabilities(AlexaEntity): if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) yield AlexaPowerLevelController(self.entity) + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" ) @@ -542,6 +543,10 @@ class FanCapabilities(AlexaEntity): yield AlexaToggleController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" ) + if supported & fan.SUPPORT_PRESET_MODE: + yield AlexaModeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}" + ) if supported & fan.SUPPORT_DIRECTION: yield AlexaModeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 79e322b4ea7..01d1369eb2f 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -958,6 +958,16 @@ async def async_api_set_mode(hass, config, directive, context): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction + # Fan preset_mode + elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": + preset_mode = mode.split(".")[1] + if preset_mode in entity.attributes.get(fan.ATTR_PRESET_MODES): + service = fan.SERVICE_SET_PRESET_MODE + data[fan.ATTR_PRESET_MODE] = preset_mode + else: + msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'" + raise AlexaInvalidValueError(msg) + # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": position = mode.split(".")[1] diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 020b03cc862..92951d4a0e7 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -400,6 +400,48 @@ async def test_report_fan_speed_state(hass): properties.assert_equal("Alexa.RangeController", "rangeValue", 3) +async def test_report_fan_preset_mode(hass): + """Test ModeController reports fan preset_mode correctly.""" + hass.states.async_set( + "fan.preset_mode", + "eco", + { + "friendly_name": "eco enabled fan", + "supported_features": 8, + "preset_mode": "eco", + "preset_modes": ["eco", "smart", "whoosh"], + }, + ) + properties = await reported_properties(hass, "fan.preset_mode") + properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.eco") + + hass.states.async_set( + "fan.preset_mode", + "smart", + { + "friendly_name": "smart enabled fan", + "supported_features": 8, + "preset_mode": "smart", + "preset_modes": ["eco", "smart", "whoosh"], + }, + ) + properties = await reported_properties(hass, "fan.preset_mode") + properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.smart") + + hass.states.async_set( + "fan.preset_mode", + "whoosh", + { + "friendly_name": "whoosh enabled fan", + "supported_features": 8, + "preset_mode": "whoosh", + "preset_modes": ["eco", "smart", "whoosh"], + }, + ) + properties = await reported_properties(hass, "fan.preset_mode") + properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.whoosh") + + async def test_report_fan_oscillating(hass): """Test ToggleController reports fan oscillating correctly.""" hass.states.async_set( diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 83abe2326d7..0da21042049 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -846,6 +846,89 @@ async def test_fan_range_off(hass): ) +async def test_preset_mode_fan(hass, caplog): + """Test fan discovery. + + This one has preset modes. + """ + device = ( + "fan.test_7", + "off", + { + "friendly_name": "Test fan 7", + "supported_features": 8, + "preset_modes": ["auto", "eco", "smart", "whoosh"], + "preset_mode": "auto", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_7" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 7" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.EndpointHealth", + "Alexa.ModeController", + "Alexa.PowerController", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.ModeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.preset_mode" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Preset"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_7", + "fan.set_preset_mode", + hass, + payload={"mode": "preset_mode.eco"}, + instance="fan.preset_mode", + ) + assert call.data["preset_mode"] == "eco" + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_7", + "fan.set_preset_mode", + hass, + payload={"mode": "preset_mode.whoosh"}, + instance="fan.preset_mode", + ) + assert call.data["preset_mode"] == "whoosh" + + with pytest.raises(AssertionError): + await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_7", + "fan.set_preset_mode", + hass, + payload={"mode": "preset_mode.invalid"}, + instance="fan.preset_mode", + ) + assert "Entity 'fan.test_7' does not support Preset 'invalid'" in caplog.text + caplog.clear() + + async def test_lock(hass): """Test lock discovery.""" device = ("lock.test", "off", {"friendly_name": "Test lock"}) @@ -2484,7 +2567,7 @@ async def test_alarm_control_panel_disarmed(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.SecurityPanelController", "Arm", "alarm_control_panel#test_1", diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 2cbf8636d79..bbe80f29eef 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -50,10 +50,13 @@ async def test_report_state_instance(hass, aioclient_mock): "off", { "friendly_name": "Test fan", - "supported_features": 3, - "speed": "off", + "supported_features": 15, + "speed": None, "speed_list": ["off", "low", "high"], "oscillating": False, + "preset_mode": None, + "preset_modes": ["auto", "smart"], + "percentage": None, }, ) @@ -64,10 +67,13 @@ async def test_report_state_instance(hass, aioclient_mock): "on", { "friendly_name": "Test fan", - "supported_features": 3, + "supported_features": 15, "speed": "high", "speed_list": ["off", "low", "high"], "oscillating": True, + "preset_mode": "smart", + "preset_modes": ["auto", "smart"], + "percentage": 90, }, ) @@ -82,11 +88,33 @@ async def test_report_state_instance(hass, aioclient_mock): assert call_json["event"]["header"]["name"] == "ChangeReport" change_reports = call_json["event"]["payload"]["change"]["properties"] + + checks = 0 for report in change_reports: if report["name"] == "toggleState": assert report["value"] == "ON" assert report["instance"] == "fan.oscillating" assert report["namespace"] == "Alexa.ToggleController" + checks += 1 + if report["name"] == "mode": + assert report["value"] == "preset_mode.smart" + assert report["instance"] == "fan.preset_mode" + assert report["namespace"] == "Alexa.ModeController" + checks += 1 + if report["name"] == "percentage": + assert report["value"] == 90 + assert report["namespace"] == "Alexa.PercentageController" + checks += 1 + if report["name"] == "powerLevel": + assert report["value"] == 90 + assert report["namespace"] == "Alexa.PowerLevelController" + checks += 1 + if report["name"] == "rangeValue": + assert report["value"] == 2 + assert report["instance"] == "fan.speed" + assert report["namespace"] == "Alexa.RangeController" + checks += 1 + assert checks == 5 assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" From 6ba2ee5cef5fe484ecae7fedd7ea408163f52cd1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 31 May 2021 23:35:33 +0200 Subject: [PATCH 102/750] Fix stream profiles not available as expected (#51305) --- homeassistant/components/axis/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 8753114d86e..d313e4b2745 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -243,8 +243,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): # Stream profiles - if vapix.params.stream_profiles_max_groups > 0: - + if vapix.stream_profiles or vapix.params.stream_profiles_max_groups > 0: stream_profiles = [DEFAULT_STREAM_PROFILE] for profile in vapix.streaming_profiles: stream_profiles.append(profile.name) From a0b3d0863b00cfe911d52c16980ea8a34fdacae0 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 31 May 2021 23:38:33 +0200 Subject: [PATCH 103/750] Fix Garmin Connect integration with python-garminconnect-aio (#50865) --- .../components/garmin_connect/__init__.py | 51 ++++++++----------- .../components/garmin_connect/config_flow.py | 19 ++++--- .../components/garmin_connect/const.py | 5 +- .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 40 ++++++++++----- 8 files changed, 67 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index af08a86abb2..bc3a1f0aad0 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,8 +1,8 @@ """The Garmin Connect integration.""" -from datetime import date, timedelta +from datetime import date import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,25 +13,27 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -MIN_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garmin Connect from a config entry.""" - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - garmin_client = Garmin(username, password) + websession = async_get_clientsession(hass) + username: str = entry.data[CONF_USERNAME] + password: str = entry.data[CONF_PASSWORD] + + garmin_client = Garmin(websession, username, password) try: - await hass.async_add_executor_job(garmin_client.login) + await garmin_client.login() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -73,38 +75,29 @@ class GarminConnectData: self.client = client self.data = None - async def _get_combined_alarms_of_all_devices(self): - """Combine the list of active alarms from all garmin devices.""" - alarms = [] - devices = await self.hass.async_add_executor_job(self.client.get_devices) - for device in devices: - device_settings = await self.hass.async_add_executor_job( - self.client.get_device_settings, device["deviceId"] - ) - alarms += device_settings["alarms"] - return alarms - - @Throttle(MIN_SCAN_INTERVAL) + @Throttle(DEFAULT_UPDATE_INTERVAL) async def async_update(self): - """Update data via library.""" + """Update data via API wrapper.""" today = date.today() try: - self.data = await self.hass.async_add_executor_job( - self.client.get_stats_and_body, today.isoformat() - ) - self.data["nextAlarm"] = await self._get_combined_alarms_of_all_devices() + summary = await self.client.get_user_summary(today.isoformat()) + body = await self.client.get_body_composition(today.isoformat()) + + self.data = { + **summary, + **body["totalAverage"], + } + self.data["nextAlarm"] = await self.client.get_device_alarms() except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, GarminConnectConnectionError, ) as err: _LOGGER.error( - "Error occurred during Garmin Connect get activity request: %s", err + "Error occurred during Garmin Connect update requests: %s", err ) - return except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unknown error occurred during Garmin Connect get activity request" + "Unknown error occurred during Garmin Connect update requests" ) - return diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 218a98ba9a4..8e26e2bf608 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect import ( +from garminconnect_aio import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -37,11 +38,15 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - garmin_client = Garmin(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + websession = async_get_clientsession(self.hass) + + garmin_client = Garmin( + websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) errors = {} try: - await self.hass.async_add_executor_job(garmin_client.login) + username = await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -56,15 +61,13 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return await self._show_setup_form(errors) - unique_id = garmin_client.get_full_name() - - await self.async_set_unique_id(unique_id) + await self.async_set_unique_id(username) self._abort_if_unique_id_configured() return self.async_create_entry( - title=unique_id, + title=username, data={ - CONF_ID: unique_id, + CONF_ID: username, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], }, diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 991ac90526a..19ed4ca4d94 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -1,4 +1,6 @@ """Constants for the Garmin Connect integration.""" +from datetime import timedelta + from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, LENGTH_METERS, @@ -8,7 +10,8 @@ from homeassistant.const import ( ) DOMAIN = "garmin_connect" -ATTRIBUTION = "Data provided by garmin.com" +ATTRIBUTION = "connect.garmin.com" +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) GARMIN_ENTITY_LIST = { "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 913e85de954..2495249e4a4 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.19"], + "requirements": ["garminconnect_aio==0.1.1"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 0d946d5e88e..eb1690c9765 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, diff --git a/requirements_all.txt b/requirements_all.txt index d6d871d3126..298934a40e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9961342391d..9c389bc742b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index f3784d5e2e2..2ad36ffa29c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry MOCK_CONF = { - CONF_ID: "First Lastname", + CONF_ID: "my@email.address", CONF_USERNAME: "my@email.address", CONF_PASSWORD: "mypassw0rd", } @@ -23,27 +23,33 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): - """Mock Garmin.""" + """Mock Garmin Connect.""" with patch( "homeassistant.components.garmin_connect.config_flow.Garmin", ) as garmin: - garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value async def test_show_form(hass): """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER -async def test_step_user(hass, mock_garmin_connect): +async def test_step_user(hass): """Test registering an integration and finishing flow works.""" with patch( + "homeassistant.components.garmin_connect.Garmin.login", + return_value="my@email.address", + ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -95,12 +101,18 @@ async def test_unknown_error(hass, mock_garmin_connect): assert result["errors"] == {"base": "unknown"} -async def test_abort_if_already_setup(hass, mock_garmin_connect): +async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ).add_to_hass(hass) + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 6631a4e605ebb0e9317a1450c36fb3b9e3c63c46 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 31 May 2021 23:39:28 +0200 Subject: [PATCH 104/750] Philips TV ambilight support (#44867) --- .coveragerc | 1 + .../components/philips_js/__init__.py | 2 +- homeassistant/components/philips_js/light.py | 380 ++++++++++++++++++ .../components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 385 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/philips_js/light.py diff --git a/.coveragerc b/.coveragerc index c3a56fa27c0..9357f6d9972 100644 --- a/.coveragerc +++ b/.coveragerc @@ -769,6 +769,7 @@ omit = homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py + homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py homeassistant/components/pi_hole/sensor.py diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index b4e086ef391..587a5f8c4f2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -PLATFORMS = ["media_player", "remote"] +PLATFORMS = ["media_player", "light", "remote"] LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py new file mode 100644 index 00000000000..800604ad7c7 --- /dev/null +++ b/homeassistant/components/philips_js/light.py @@ -0,0 +1,380 @@ +"""Component to integrate ambilight for TVs exposing the Joint Space API.""" +from __future__ import annotations + +from typing import Any + +from haphilipsjs import PhilipsTV +from haphilipsjs.typing import AmbilightCurrentConfiguration + +from homeassistant import config_entries +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_EFFECT, + LightEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv + +from . import PhilipsTVDataUpdateCoordinator +from .const import CONF_SYSTEM, DOMAIN + +EFFECT_PARTITION = ": " +EFFECT_MODE = "Mode" +EFFECT_EXPERT = "Expert" +EFFECT_AUTO = "Auto" +EFFECT_EXPERT_STYLES = {"FOLLOW_AUDIO", "FOLLOW_COLOR", "Lounge light"} + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: config_entries.ConfigEntry, + async_add_entities, +): + """Set up the configuration entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + PhilipsTVLightEntity( + coordinator, config_entry.data[CONF_SYSTEM], config_entry.unique_id + ) + ] + ) + + +def _get_settings(style: AmbilightCurrentConfiguration): + """Extract the color settings data from a style.""" + if style["styleName"] in ("FOLLOW_COLOR", "Lounge light"): + return style["colorSettings"] + if style["styleName"] == "FOLLOW_AUDIO": + return style["audioSettings"] + return None + + +def _parse_effect(effect: str): + style, _, algorithm = effect.partition(EFFECT_PARTITION) + if style == EFFECT_MODE: + return EFFECT_MODE, algorithm, None + algorithm, _, expert = algorithm.partition(EFFECT_PARTITION) + if expert: + return EFFECT_EXPERT, style, algorithm + return EFFECT_AUTO, style, algorithm + + +def _get_effect(mode: str, style: str, algorithm: str | None): + if mode == EFFECT_MODE: + return f"{EFFECT_MODE}{EFFECT_PARTITION}{style}" + if mode == EFFECT_EXPERT: + return f"{style}{EFFECT_PARTITION}{algorithm}{EFFECT_PARTITION}{EFFECT_EXPERT}" + return f"{style}{EFFECT_PARTITION}{algorithm}" + + +def _is_on(mode, style, powerstate): + if mode in (EFFECT_AUTO, EFFECT_EXPERT): + if style in ("FOLLOW_VIDEO", "FOLLOW_AUDIO"): + return powerstate in ("On", None) + if style == "OFF": + return False + return True + + if mode == EFFECT_MODE: + if style == "internal": + return powerstate in ("On", None) + return True + + return False + + +def _is_valid(mode, style): + if mode == EFFECT_EXPERT: + return style in EFFECT_EXPERT_STYLES + return True + + +def _get_cache_keys(device: PhilipsTV): + """Return a cache keys to avoid always updating.""" + return ( + device.on, + device.powerstate, + device.ambilight_current_configuration, + device.ambilight_mode, + ) + + +def _average_pixels(data): + """Calculate an average color over all ambilight pixels.""" + color_c = 0 + color_r = 0.0 + color_g = 0.0 + color_b = 0.0 + for layer in data.values(): + for side in layer.values(): + for pixel in side.values(): + color_c += 1 + color_r += pixel["r"] + color_g += pixel["g"] + color_b += pixel["b"] + + if color_c: + color_r /= color_c + color_g /= color_c + color_b /= color_c + return color_r, color_g, color_b + return 0.0, 0.0, 0.0 + + +class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): + """Representation of a Philips TV exposing the JointSpace API.""" + + def __init__( + self, + coordinator: PhilipsTVDataUpdateCoordinator, + system: dict[str, Any], + unique_id: str, + ) -> None: + """Initialize light.""" + self._tv = coordinator.api + self._hs = None + self._brightness = None + self._system = system + self._coordinator = coordinator + self._cache_keys = None + super().__init__(coordinator) + + self._attr_supported_color_modes = [COLOR_MODE_HS, COLOR_MODE_ONOFF] + self._attr_supported_features = ( + SUPPORT_EFFECT | SUPPORT_COLOR | SUPPORT_BRIGHTNESS + ) + self._attr_name = self._system["name"] + self._attr_unique_id = unique_id + self._attr_icon = "mdi:television-ambient-light" + self._attr_device_info = { + "name": self._system["name"], + "identifiers": { + (DOMAIN, self._attr_unique_id), + }, + "model": self._system.get("model"), + "manufacturer": "Philips", + "sw_version": self._system.get("softwareversion"), + } + + self._update_from_coordinator() + + def _calculate_effect_list(self): + """Calculate an effect list based on current status.""" + effects = [] + effects.extend( + _get_effect(EFFECT_AUTO, style, setting) + for style, data in self._tv.ambilight_styles.items() + if _is_valid(EFFECT_AUTO, style) + and _is_on(EFFECT_AUTO, style, self._tv.powerstate) + for setting in data.get("menuSettings", []) + ) + + effects.extend( + _get_effect(EFFECT_EXPERT, style, algorithm) + for style, data in self._tv.ambilight_styles.items() + if _is_valid(EFFECT_EXPERT, style) + and _is_on(EFFECT_EXPERT, style, self._tv.powerstate) + for algorithm in data.get("algorithms", []) + ) + + effects.extend( + _get_effect(EFFECT_MODE, style, None) + for style in self._tv.ambilight_modes + if _is_valid(EFFECT_MODE, style) + and _is_on(EFFECT_MODE, style, self._tv.powerstate) + ) + + return sorted(effects) + + def _calculate_effect(self): + """Return the current effect.""" + current = self._tv.ambilight_current_configuration + if current and self._tv.ambilight_mode != "manual": + if current["isExpert"]: + settings = _get_settings(current) + if settings: + return _get_effect( + EFFECT_EXPERT, current["styleName"], settings["algorithm"] + ) + return _get_effect(EFFECT_EXPERT, current["styleName"], None) + + return _get_effect( + EFFECT_AUTO, current["styleName"], current.get("menuSetting", None) + ) + + return _get_effect(EFFECT_MODE, self._tv.ambilight_mode, None) + + @property + def color_mode(self): + """Return the current color mode.""" + current = self._tv.ambilight_current_configuration + if current and current["isExpert"]: + return COLOR_MODE_HS + + if self._tv.ambilight_mode in ["manual", "expert"]: + return COLOR_MODE_HS + + return COLOR_MODE_ONOFF + + @property + def is_on(self): + """Return if the light is turned on.""" + if self._tv.on: + mode, style, _ = _parse_effect(self.effect) + return _is_on(mode, style, self._tv.powerstate) + + return False + + def _update_from_coordinator(self): + current = self._tv.ambilight_current_configuration + color = None + if current and current["isExpert"]: + if settings := _get_settings(current): + color = settings["color"] + + if color: + self._attr_hs_color = ( + color["hue"] * 360.0 / 255.0, + color["saturation"] * 100.0 / 255.0, + ) + self._attr_brightness = color["brightness"] + elif data := self._tv.ambilight_cached: + hsv_h, hsv_s, hsv_v = color_RGB_to_hsv(*_average_pixels(data)) + self._attr_hs_color = hsv_h, hsv_s + self._attr_brightness = hsv_v * 255.0 / 100.0 + else: + self._attr_hs_color = None + self._attr_brightness = None + + if (cache_keys := _get_cache_keys(self._tv)) != self._cache_keys: + self._cache_keys = cache_keys + self._attr_effect_list = self._calculate_effect_list() + self._attr_effect = self._calculate_effect() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_from_coordinator() + super()._handle_coordinator_update() + + async def _set_ambilight_cached(self, algorithm, hs_color, brightness): + """Set ambilight via the manual or expert mode.""" + rgb = color_hsv_to_RGB(hs_color[0], hs_color[1], brightness * 100 / 255) + + data = { + "r": rgb[0], + "g": rgb[1], + "b": rgb[2], + } + + if not await self._tv.setAmbilightCached(data): + raise Exception("Failed to set ambilight color") + + if algorithm != self._tv.ambilight_mode: + if not await self._tv.setAmbilightMode(algorithm): + raise Exception("Failed to set ambilight mode") + + async def _set_ambilight_expert_config( + self, style, algorithm, hs_color, brightness + ): + """Set ambilight via current configuration.""" + config: AmbilightCurrentConfiguration = { + "styleName": style, + "isExpert": True, + } + + setting = { + "algorithm": algorithm, + "color": { + "hue": round(hs_color[0] * 255.0 / 360.0), + "saturation": round(hs_color[1] * 255.0 / 100.0), + "brightness": round(brightness), + }, + "colorDelta": { + "hue": 0, + "saturation": 0, + "brightness": 0, + }, + } + + if style in ("FOLLOW_COLOR", "Lounge light"): + config["colorSettings"] = setting + config["speed"] = 2 + + elif style == "FOLLOW_AUDIO": + config["audioSettings"] = setting + config["tuning"] = 0 + + if not await self._tv.setAmbilightCurrentConfiguration(config): + raise Exception("Failed to set ambilight mode") + + async def _set_ambilight_config(self, style, algorithm): + """Set ambilight via current configuration.""" + config: AmbilightCurrentConfiguration = { + "styleName": style, + "isExpert": False, + "menuSetting": algorithm, + } + + if await self._tv.setAmbilightCurrentConfiguration(config) is False: + raise Exception("Failed to set ambilight mode") + + async def async_turn_on(self, **kwargs) -> None: + """Turn the bulb on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) + effect = kwargs.get(ATTR_EFFECT, self.effect) + + if not self._tv.on: + raise Exception("TV is not available") + + mode, style, setting = _parse_effect(effect) + + if not _is_on(mode, style, self._tv.powerstate): + mode = EFFECT_MODE + setting = None + if self._tv.powerstate in ("On", None): + style = "internal" + else: + style = "manual" + + if brightness is None: + brightness = 255 + + if hs_color is None: + hs_color = [0, 0] + + if mode == EFFECT_MODE: + await self._set_ambilight_cached(style, hs_color, brightness) + elif mode == EFFECT_AUTO: + await self._set_ambilight_config(style, setting) + elif mode == EFFECT_EXPERT: + await self._set_ambilight_expert_config( + style, setting, hs_color, brightness + ) + + self._update_from_coordinator() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn of ambilight.""" + + if not self._tv.on: + raise Exception("TV is not available") + + if await self._tv.setAmbilightMode("internal") is False: + raise Exception("Failed to set ambilight mode") + + await self._set_ambilight_config("OFF", "") + + self._update_from_coordinator() + self.async_write_ha_state() diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 9d1c4dbd04d..4f3ee5a9ab3 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.7.3"], + "requirements": ["ha-philipsjs==2.7.4"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 298934a40e4..68bc6e37a4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.3 +ha-philipsjs==2.7.4 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c389bc742b..b881db6904c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ guppy3==3.1.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.7.3 +ha-philipsjs==2.7.4 # homeassistant.components.habitica habitipy==0.2.0 From 95362d421531618102c25ad685f027fafd5417e2 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 31 May 2021 23:28:14 +0100 Subject: [PATCH 105/750] Bump aiohomekit to 0.2.66 (#51310) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 46fe126ebf0..c18ee9e574f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.65"], + "requirements": ["aiohomekit==0.2.66"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 68bc6e37a4d..3a9fe64ef62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b881db6904c..c89bc38267e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.65 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http From 5d6b6deed4374865143d182b5dd63a4b8a362388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 00:32:03 +0200 Subject: [PATCH 106/750] Move version validation to resolver (#51311) --- homeassistant/loader.py | 60 +++++++++---------- tests/test_loader.py | 57 +++++++----------- .../test_bad_version/__init__.py | 1 + .../test_bad_version/manifest.json | 4 ++ .../test_no_version/__init__.py | 1 + .../test_no_version/manifest.json | 3 + 6 files changed, 58 insertions(+), 68 deletions(-) create mode 100644 tests/testing_config/custom_components/test_bad_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_bad_version/manifest.json create mode 100644 tests/testing_config/custom_components/test_no_version/__init__.py create mode 100644 tests/testing_config/custom_components/test_no_version/manifest.json diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 444e35add33..06bf5045c9f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -47,7 +47,7 @@ DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( - "You are using a custom integration %s which has not " + "We found a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" @@ -290,13 +290,39 @@ class Integration: ) continue - return cls( + integration = cls( hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest, ) + if integration.is_built_in: + return integration + + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], + ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + return None + return integration + return None def __init__( @@ -523,8 +549,6 @@ async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integratio # Instead of using resolve_from_root we use the cache of custom # components to find the integration. if integration := (await async_get_custom_components(hass)).get(domain): - validate_custom_integration_version(integration) - _LOGGER.warning(CUSTOM_WARNING, integration.domain) return integration from homeassistant import components # pylint: disable=import-outside-toplevel @@ -744,31 +768,3 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] - - -def validate_custom_integration_version(integration: Integration) -> None: - """ - Validate the version of custom integrations. - - Raises IntegrationNotFound when version is missing or not valid - """ - try: - AwesomeVersion( - integration.version, - [ - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ], - ) - except AwesomeVersionException: - _LOGGER.error( - "The custom integration '%s' does not have a " - "valid version key (%s) in the manifest file and was blocked from loading. " - "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", - integration.domain, - integration.version, - ) - raise IntegrationNotFound(integration.domain) from None diff --git a/tests/test_loader.py b/tests/test_loader.py index e696f27351d..20dcf90d90e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -127,37 +127,30 @@ async def test_log_warning_custom_component(hass, caplog, enable_custom_integrat """Test that we log a warning when loading a custom component.""" await loader.async_get_integration(hass, "test_package") - assert "You are using a custom integration test_package" in caplog.text + assert "We found a custom integration test_package" in caplog.text await loader.async_get_integration(hass, "test") - assert "You are using a custom integration test " in caplog.text + assert "We found a custom integration test " in caplog.text -async def test_custom_integration_version_not_valid(hass, caplog): +async def test_custom_integration_version_not_valid( + hass, caplog, enable_custom_integrations +): """Test that we log a warning when custom integrations have a invalid version.""" - test_integration1 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test1", "version": "test"} - ) - test_integration2 = loader.Integration( - hass, "custom_components.test", None, {"domain": "test2"} + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + assert ( + "The custom integration 'test_no_version' does not have a valid version key (None) in the manifest file and was blocked from loading." + in caplog.text ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = {"test1": test_integration1, "test2": test_integration2} - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - assert ( - "The custom integration 'test1' does not have a valid version key (test) in the manifest file and was blocked from loading." - in caplog.text - ) - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test2") - assert ( - "The custom integration 'test2' does not have a valid version key (None) in the manifest file and was blocked from loading." - in caplog.text - ) + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test2") + assert ( + "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." + in caplog.text + ) async def test_get_integration(hass): @@ -471,19 +464,11 @@ async def test_get_custom_components_safe_mode(hass): async def test_custom_integration_missing_version(hass, caplog): """Test trying to load a custom integration without a version twice does not deadlock.""" - test_integration_1 = loader.Integration( - hass, "custom_components.test1", None, {"domain": "test1"} - ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = { - "test1": test_integration_1, - } + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") - - with pytest.raises(loader.IntegrationNotFound): - await loader.async_get_integration(hass, "test1") + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") async def test_custom_integration_missing(hass, caplog): diff --git a/tests/testing_config/custom_components/test_bad_version/__init__.py b/tests/testing_config/custom_components/test_bad_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_bad_version/manifest.json b/tests/testing_config/custom_components/test_bad_version/manifest.json new file mode 100644 index 00000000000..69d322a33ad --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_bad_version", + "version": "bad" +} \ No newline at end of file diff --git a/tests/testing_config/custom_components/test_no_version/__init__.py b/tests/testing_config/custom_components/test_no_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_no_version/manifest.json b/tests/testing_config/custom_components/test_no_version/manifest.json new file mode 100644 index 00000000000..9054cf4f5e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/manifest.json @@ -0,0 +1,3 @@ +{ + "domain": "test_no_version" +} \ No newline at end of file From 4821484d2c34ee2f1b3c4e200e85e950844fc2e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 15:36:40 -0700 Subject: [PATCH 107/750] Add system option to disable polling (#51299) --- .../components/config/config_entries.py | 36 +++++---- homeassistant/config_entries.py | 27 +++++-- homeassistant/helpers/entity_platform.py | 7 +- homeassistant/helpers/update_coordinator.py | 9 ++- .../components/config/test_config_entries.py | 73 +++++++++++++------ tests/helpers/test_entity_platform.py | 13 ++++ tests/helpers/test_update_coordinator.py | 12 ++- 7 files changed, 123 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index efc60288439..9d88a9b5311 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -31,7 +31,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) @@ -231,20 +230,6 @@ def config_entries_progress(hass, connection, msg): ) -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/system_options/list", "entry_id": str} -) -async def system_options_list(hass, connection, msg): - """List all system options for a config entry.""" - entry_id = msg["entry_id"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry: - connection.send_result(msg["id"], entry.system_options.as_dict()) - - def send_entry_not_found(connection, msg_id): """Send Config entry not found error.""" connection.send_error( @@ -267,6 +252,7 @@ def get_entry(hass, connection, entry_id, msg_id): "type": "config_entries/system_options/update", "entry_id": str, vol.Optional("disable_new_entities"): bool, + vol.Optional("disable_polling"): bool, } ) async def system_options_update(hass, connection, msg): @@ -280,8 +266,25 @@ async def system_options_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.system_options.disable_polling + hass.config_entries.async_update_entry(entry, system_options=changes) - connection.send_result(msg["id"], entry.system_options.as_dict()) + + result = { + "system_options": entry.system_options.as_dict(), + "require_restart": False, + } + + if ( + old_disable_polling != entry.system_options.disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -388,6 +391,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, + "system_options": entry.system_options.as_dict(), "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b3589d03b92..33c18fc0d7c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -994,12 +994,10 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if ( - system_options is not UNDEFINED - and entry.system_options.as_dict() != system_options - ): - changed = True + if system_options is not UNDEFINED: + old_system_options = entry.system_options.as_dict() entry.system_options.update(**system_options) + changed = entry.system_options.as_dict() != old_system_options if not changed: return False @@ -1408,14 +1406,27 @@ class SystemOptions: """Config entry system options.""" disable_new_entities: bool = attr.ib(default=False) + disable_polling: bool = attr.ib(default=False) - def update(self, *, disable_new_entities: bool) -> None: + def update( + self, + *, + disable_new_entities: bool | UndefinedType = UNDEFINED, + disable_polling: bool | UndefinedType = UNDEFINED, + ) -> None: """Update properties.""" - self.disable_new_entities = disable_new_entities + if disable_new_entities is not UNDEFINED: + self.disable_new_entities = disable_new_entities + + if disable_polling is not UNDEFINED: + self.disable_polling = disable_polling def as_dict(self) -> dict[str, Any]: """Return dictionary version of this config entries system options.""" - return {"disable_new_entities": self.disable_new_entities} + return { + "disable_new_entities": self.disable_new_entities, + "disable_polling": self.disable_polling, + } class EntityRegistryDisabledHandler: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 26bfdb43e66..f0d691a1c8d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -214,6 +214,7 @@ class EntityPlatform: @callback def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" + config_entries.current_entry.set(config_entry) return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -395,8 +396,10 @@ class EntityPlatform: ) raise - if self._async_unsub_polling is not None or not any( - entity.should_poll for entity in self.entities.values() + if ( + (self.config_entry and self.config_entry.system_options.disable_polling) + or self._async_unsub_polling is not None + or not any(entity.should_poll for entity in self.entities.values()) ): return diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c15d6534626..e91acfaf82f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -49,6 +49,7 @@ class DataUpdateCoordinator(Generic[T]): self.name = name self.update_method = update_method self.update_interval = update_interval + self.config_entry = config_entries.current_entry.get() # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -110,6 +111,9 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return + if self.config_entry and self.config_entry.system_options.disable_polling: + return + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -229,9 +233,8 @@ class DataUpdateCoordinator(Generic[T]): if raise_on_auth_failed: raise - config_entry = config_entries.current_entry.get() - if config_entry: - config_entry.async_start_reauth(self.hass) + if self.config_entry: + self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 10fc3aadba0..570d847e86e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,6 +87,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": None, }, @@ -97,6 +101,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": None, "reason": "Unsupported API", }, @@ -107,6 +115,10 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -328,6 +340,10 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "Test Entry", "reason": None, }, @@ -399,6 +415,10 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "system_options": { + "disable_new_entities": False, + "disable_polling": False, + }, "title": "user-title", "reason": None, }, @@ -678,35 +698,17 @@ async def test_two_step_options_flow(hass, client): } -async def test_list_system_options(hass, hass_ws_client): - """Test that we can list an entries system options.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) - - entry = MockConfigEntry(domain="demo") - entry.add_to_hass(hass) - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/list", - "entry_id": entry.entry_id, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == {"disable_new_entities": False} - - async def test_update_system_options(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is False + await ws_client.send_json( { "id": 5, @@ -718,8 +720,31 @@ async def test_update_system_options(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["disable_new_entities"] - assert entry.system_options.disable_new_entities + assert response["result"] == { + "require_restart": False, + "system_options": {"disable_new_entities": True, "disable_polling": False}, + } + assert entry.system_options.disable_new_entities is True + assert entry.system_options.disable_polling is False + + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": False, + "disable_polling": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == { + "require_restart": True, + "system_options": {"disable_new_entities": False, "disable_polling": True}, + } + assert entry.system_options.disable_new_entities is False + assert entry.system_options.disable_polling is True async def test_update_system_options_nonexisting(hass, hass_ws_client): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 6675e441adf..944f02d46c0 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -57,6 +57,19 @@ async def test_polling_only_updates_entities_it_should_poll(hass): assert poll_ent.async_update.called +async def test_polling_disabled_by_config_entry(hass): + """Test the polling of only updated entities.""" + entity_platform = MockEntityPlatform(hass) + entity_platform.config_entry = MockConfigEntry( + system_options={"disable_polling": True} + ) + + poll_ent = MockEntity(should_poll=True) + + await entity_platform.async_add_entities([poll_ent]) + assert entity_platform._async_unsub_polling is None + + async def test_polling_updates_entities_with_exception(hass): """Test the updated entities that not break with an exception.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 244e221f53a..a0ce751aed8 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,13 +9,14 @@ import aiohttp import pytest import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -371,3 +372,12 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): await crd.async_config_entry_first_refresh() assert crd.last_update_success is True + + +async def test_not_schedule_refresh_if_system_option_disable_polling(hass): + """Test we do not schedule a refresh if disable polling in config entry.""" + entry = MockConfigEntry(system_options={"disable_polling": True}) + config_entries.current_entry.set(entry) + crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) + crd.async_add_listener(lambda: None) + assert crd._unsub_refresh is None From 354dd39f24e5109b41da92ae3c6178876dbd6118 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:08 -0700 Subject: [PATCH 108/750] Updated frontend to 20210531.1 (#51314) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 61da986b06b..49b51a7864c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210531.0" + "home-assistant-frontend==20210531.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0ae8841b186..abf13fa339e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3a9fe64ef62..881204da143 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c89bc38267e..feb3d094e49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.0 +home-assistant-frontend==20210531.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f472219c68ad43592e8ff295722cc4a01819e999 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 May 2021 16:35:31 -0700 Subject: [PATCH 109/750] Set up cloud semi-dependencies at start (#51313) --- .../components/cloud/alexa_config.py | 10 +++-- .../components/cloud/google_config.py | 9 +++-- homeassistant/core.py | 4 +- homeassistant/helpers/start.py | 25 ++++++++++++ tests/components/cloud/test_google_config.py | 1 + tests/helpers/test_start.py | 39 +++++++++++++++++++ 6 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 homeassistant/helpers/start.py create mode 100644 tests/helpers/test_start.py diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index c7568d7ae25..7394936f355 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -17,7 +17,7 @@ from homeassistant.components.alexa import ( ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -107,8 +107,12 @@ class AlexaConfig(alexa_config.AbstractConfig): async def async_initialize(self): """Initialize the Alexa config.""" - if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + async def hass_started(hass): + if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) def should_expose(self, entity_id): """If an entity should be exposed.""" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 41f62c32c39..65cbe8bb342 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -9,7 +9,7 @@ from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOM from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry, start from homeassistant.setup import async_setup_component from .const import ( @@ -86,8 +86,11 @@ class CloudGoogleConfig(AbstractConfig): """Perform async initialization of config.""" await super().async_initialize() - if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: - await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + async def hass_started(hass): + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + + start.async_at_start(self.hass, hass_started) # Remove old/wrong user agent ids remove_agent_user_ids = [] diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b5c93b15bb..b9bf97e7e6c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -374,7 +374,7 @@ class HomeAssistant: return task - def create_task(self, target: Coroutine) -> None: + def create_task(self, target: Awaitable) -> None: """Add task to the executor pool. target: target to call. @@ -382,7 +382,7 @@ class HomeAssistant: self.loop.call_soon_threadsafe(self.async_create_task, target) @callback - def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: + def async_create_task(self, target: Awaitable) -> asyncio.tasks.Task: """Create a task from within the eventloop. This method must be run in the event loop. diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py new file mode 100644 index 00000000000..e7e827ec5c3 --- /dev/null +++ b/homeassistant/helpers/start.py @@ -0,0 +1,25 @@ +"""Helpers to help during startup.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import Event, HomeAssistant, callback + + +@callback +def async_at_start( + hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable] +) -> None: + """Execute something when Home Assistant is started. + + Will execute it now if Home Assistant is already started. + """ + if hass.is_running: + hass.async_create_task(at_start_cb(hass)) + return + + async def _matched_event(event: Event) -> None: + """Call the callback when Home Assistant started.""" + await at_start_cb(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index bc430347e08..64d50250259 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -234,6 +234,7 @@ async def test_setup_integration(hass, mock_conf, cloud_prefs): assert "google_assistant" not in hass.config.components await mock_conf.async_initialize() + await hass.async_block_till_done() assert "google_assistant" in hass.config.components hass.config.components.remove("google_assistant") diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py new file mode 100644 index 00000000000..35838f1ceaa --- /dev/null +++ b/tests/helpers/test_start.py @@ -0,0 +1,39 @@ +"""Test starting HA helpers.""" +from homeassistant import core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.helpers import start + + +async def test_at_start_when_running(hass): + """Test at start when already running.""" + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_start_when_starting(hass): + """Test at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 1 From 2a746acf3a0296103f1de5898a63fe550b9b4d82 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 1 Jun 2021 00:30:06 +0000 Subject: [PATCH 110/750] [ci skip] Translation update --- homeassistant/components/zha/translations/et.json | 2 ++ homeassistant/components/zha/translations/no.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index afbf2180ef6..311a6378fcb 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -41,6 +41,8 @@ "title": "Valvekeskuse juhtpaneeli s\u00e4tted" }, "zha_options": { + "consider_unavailable_battery": "Arvesta, et patareitoitega seadmed pole p\u00e4rast (sekundit) saadaval", + "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", "title": "\u00dcldised valikud" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 087b4adb2c3..efa6c06a067 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -41,6 +41,8 @@ "title": "Alternativer for alarmkontrollpanel" }, "zha_options": { + "consider_unavailable_battery": "Vurder batteridrevne enheter som utilgjengelige etter (sekunder)", + "consider_unavailable_mains": "Tenk p\u00e5 str\u00f8mnettet som ikke er tilgjengelig etter (sekunder)", "default_light_transition": "Standard lysovergangstid (sekunder)", "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", "title": "Globale alternativer" From 0e0da268524e2ef84785db01b08a08ee5e4ae306 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Tue, 1 Jun 2021 02:44:56 -0400 Subject: [PATCH 111/750] update adext dependency (#51315) --- homeassistant/components/alarmdecoder/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index fa2bcca389f..a762d698545 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,7 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["adext==0.4.1"], + "requirements": ["adext==0.4.2"], "codeowners": ["@ajschmidt8"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 881204da143..2054159d430 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -108,7 +108,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index feb3d094e49..9af5fb43134 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ accuweather==0.2.0 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 From fb682665e2d85fe099e9cfba4adc34969154063e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 08:48:53 +0200 Subject: [PATCH 112/750] Upgrade pylint to 2.8.3 (#51308) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 02b041d6074..142bc879806 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 pre-commit==2.13.0 -pylint==2.8.2 +pylint==2.8.3 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 164e45f0a7fd75bd8498e441eacd872e690d4a8e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 1 Jun 2021 08:59:23 +0200 Subject: [PATCH 113/750] KNX: move some Schema to schema.py (#51307) * create platform schema node from schema class * move connection schema to schema.py * rename SCHEMA to ENTITY_SCHEMA * Final module level constants --- homeassistant/components/knx/__init__.py | 118 +++++++---------------- homeassistant/components/knx/const.py | 27 +++--- homeassistant/components/knx/fan.py | 4 +- homeassistant/components/knx/schema.py | 112 ++++++++++++++++----- 4 files changed, 143 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e98e598af1d..c30b24475c0 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -3,18 +3,14 @@ from __future__ import annotations import asyncio import logging +from typing import Final import voluptuous as vol from xknx import XKNX from xknx.core.telegram_queue import TelegramQueue from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import XKNXException -from xknx.io import ( - DEFAULT_MCAST_GRP, - DEFAULT_MCAST_PORT, - ConnectionConfig, - ConnectionType, -) +from xknx.io import ConnectionConfig, ConnectionType from xknx.telegram import AddressFilter, Telegram from xknx.telegram.address import parse_device_group_address from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite @@ -34,7 +30,15 @@ from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, KNX_ADDRESS, SupportedPlatforms +from .const import ( + CONF_KNX_EXPOSE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, + DOMAIN, + KNX_ADDRESS, + SupportedPlatforms, +) from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure from .schema import ( BinarySensorSchema, @@ -50,30 +54,22 @@ from .schema import ( SwitchSchema, WeatherSchema, ga_validator, - ia_validator, sensor_type_validator, ) _LOGGER = logging.getLogger(__name__) -CONF_KNX_ROUTING = "routing" -CONF_KNX_TUNNELING = "tunneling" -CONF_KNX_FIRE_EVENT = "fire_event" -CONF_KNX_EVENT_FILTER = "event_filter" -CONF_KNX_INDIVIDUAL_ADDRESS = "individual_address" -CONF_KNX_MCAST_GRP = "multicast_group" -CONF_KNX_MCAST_PORT = "multicast_port" -CONF_KNX_STATE_UPDATER = "state_updater" -CONF_KNX_RATE_LIMIT = "rate_limit" -CONF_KNX_EXPOSE = "expose" -SERVICE_KNX_SEND = "send" -SERVICE_KNX_ATTR_PAYLOAD = "payload" -SERVICE_KNX_ATTR_TYPE = "type" -SERVICE_KNX_ATTR_REMOVE = "remove" -SERVICE_KNX_EVENT_REGISTER = "event_register" -SERVICE_KNX_EXPOSURE_REGISTER = "exposure_register" -SERVICE_KNX_READ = "read" +CONF_KNX_FIRE_EVENT: Final = "fire_event" +CONF_KNX_EVENT_FILTER: Final = "event_filter" + +SERVICE_KNX_SEND: Final = "send" +SERVICE_KNX_ATTR_PAYLOAD: Final = "payload" +SERVICE_KNX_ATTR_TYPE: Final = "type" +SERVICE_KNX_ATTR_REMOVE: Final = "remove" +SERVICE_KNX_EVENT_REGISTER: Final = "event_register" +SERVICE_KNX_EXPOSURE_REGISTER: Final = "exposure_register" +SERVICE_KNX_READ: Final = "read" CONFIG_SCHEMA = vol.Schema( { @@ -85,62 +81,22 @@ CONFIG_SCHEMA = vol.Schema( cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), vol.Schema( { - vol.Exclusive( - CONF_KNX_ROUTING, "connection_type" - ): ConnectionSchema.ROUTING_SCHEMA, - vol.Exclusive( - CONF_KNX_TUNNELING, "connection_type" - ): ConnectionSchema.TUNNELING_SCHEMA, + **ConnectionSchema.SCHEMA, vol.Optional(CONF_KNX_FIRE_EVENT): cv.boolean, vol.Optional(CONF_KNX_EVENT_FILTER, default=[]): vol.All( cv.ensure_list, [cv.string] ), - vol.Optional( - CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): ia_validator, - vol.Optional( - CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP - ): cv.string, - vol.Optional( - CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT - ): cv.port, - vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, - vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_KNX_EXPOSE): vol.All( - cv.ensure_list, [ExposeSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.COVER.value): vol.All( - cv.ensure_list, [CoverSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.BINARY_SENSOR.value): vol.All( - cv.ensure_list, [BinarySensorSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.LIGHT.value): vol.All( - cv.ensure_list, [LightSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.CLIMATE.value): vol.All( - cv.ensure_list, [ClimateSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.NOTIFY.value): vol.All( - cv.ensure_list, [NotifySchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.SWITCH.value): vol.All( - cv.ensure_list, [SwitchSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.SENSOR.value): vol.All( - cv.ensure_list, [SensorSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.SCENE.value): vol.All( - cv.ensure_list, [SceneSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.WEATHER.value): vol.All( - cv.ensure_list, [WeatherSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.FAN.value): vol.All( - cv.ensure_list, [FanSchema.SCHEMA] - ), + **ExposeSchema.platform_node(), + **BinarySensorSchema.platform_node(), + **ClimateSchema.platform_node(), + **CoverSchema.platform_node(), + **FanSchema.platform_node(), + **LightSchema.platform_node(), + **NotifySchema.platform_node(), + **SceneSchema.platform_node(), + **SensorSchema.platform_node(), + **SwitchSchema.platform_node(), + **WeatherSchema.platform_node(), } ), ) @@ -315,11 +271,11 @@ class KNXModule: """Initialize XKNX object.""" self.xknx = XKNX( own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], - rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT], - multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP], - multicast_port=self.config[DOMAIN][CONF_KNX_MCAST_PORT], + rate_limit=self.config[DOMAIN][ConnectionSchema.CONF_KNX_RATE_LIMIT], + multicast_group=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_GRP], + multicast_port=self.config[DOMAIN][ConnectionSchema.CONF_KNX_MCAST_PORT], connection_config=self.connection_config(), - state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], + state_updater=self.config[DOMAIN][ConnectionSchema.CONF_KNX_STATE_UPDATER], ) async def start(self) -> None: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 78b3f5ec7f9..4935cbd09d4 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -1,5 +1,6 @@ """Constants for the KNX integration.""" from enum import Enum +from typing import Final from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, @@ -15,19 +16,23 @@ from homeassistant.components.climate.const import ( PRESET_SLEEP, ) -DOMAIN = "knx" +DOMAIN: Final = "knx" # Address is used for configuration and services by the same functions so the key has to match -KNX_ADDRESS = "address" +KNX_ADDRESS: Final = "address" -CONF_INVERT = "invert" -CONF_STATE_ADDRESS = "state_address" -CONF_SYNC_STATE = "sync_state" -CONF_RESET_AFTER = "reset_after" +CONF_KNX_ROUTING: Final = "routing" +CONF_KNX_TUNNELING: Final = "tunneling" +CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" +CONF_INVERT: Final = "invert" +CONF_KNX_EXPOSE: Final = "expose" +CONF_STATE_ADDRESS: Final = "state_address" +CONF_SYNC_STATE: Final = "sync_state" +CONF_RESET_AFTER: Final = "reset_after" -ATTR_COUNTER = "counter" -ATTR_SOURCE = "source" -ATTR_LAST_KNX_UPDATE = "last_knx_update" +ATTR_COUNTER: Final = "counter" +ATTR_SOURCE: Final = "source" +ATTR_LAST_KNX_UPDATE: Final = "last_knx_update" class ColorTempModes(Enum): @@ -53,7 +58,7 @@ class SupportedPlatforms(Enum): # Map KNX controller modes to HA modes. This list might not be complete. -CONTROLLER_MODES = { +CONTROLLER_MODES: Final = { # Map DPT 20.105 HVAC control modes "Auto": HVAC_MODE_AUTO, "Heat": HVAC_MODE_HEAT, @@ -63,7 +68,7 @@ CONTROLLER_MODES = { "Dry": HVAC_MODE_DRY, } -PRESET_MODES = { +PRESET_MODES: Final = { # Map DPT 20.102 HVAC operating modes to HA presets "Auto": PRESET_NONE, "Frost Protection": PRESET_ECO, diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 4b4a84c26d2..b3c19d2cfd6 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -2,7 +2,7 @@ from __future__ import annotations import math -from typing import Any +from typing import Any, Final from xknx import XKNX from xknx.devices import Fan as XknxFan @@ -22,7 +22,7 @@ from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import FanSchema -DEFAULT_PERCENTAGE = 50 +DEFAULT_PERCENTAGE: Final = 50 async def async_setup_platform( diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 3ac0ec84e1d..fa94f503d7e 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -1,13 +1,15 @@ """Voluptuous schemas for the KNX integration.""" from __future__ import annotations -from typing import Any +from abc import ABC +from typing import Any, ClassVar import voluptuous as vol +from xknx import XKNX from xknx.devices.climate import SetpointShiftMode from xknx.dpt import DPTBase from xknx.exceptions import CouldNotParseAddress -from xknx.io import DEFAULT_MCAST_PORT +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.components.binary_sensor import ( @@ -26,6 +28,10 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_INVERT, + CONF_KNX_EXPOSE, + CONF_KNX_INDIVIDUAL_ADDRESS, + CONF_KNX_ROUTING, + CONF_KNX_TUNNELING, CONF_RESET_AFTER, CONF_STATE_ADDRESS, CONF_SYNC_STATE, @@ -33,6 +39,7 @@ from .const import ( KNX_ADDRESS, PRESET_MODES, ColorTempModes, + SupportedPlatforms, ) ################## @@ -76,6 +83,7 @@ sync_state_validator = vol.Any( cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) + ############## # CONNECTION ############## @@ -85,7 +93,11 @@ class ConnectionSchema: """Voluptuous schema for KNX connection.""" CONF_KNX_LOCAL_IP = "local_ip" + CONF_KNX_MCAST_GRP = "multicast_group" + CONF_KNX_MCAST_PORT = "multicast_port" + CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_ROUTE_BACK = "route_back" + CONF_KNX_STATE_UPDATER = "state_updater" TUNNELING_SCHEMA = vol.Schema( { @@ -98,15 +110,47 @@ class ConnectionSchema: ROUTING_SCHEMA = vol.Maybe(vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string})) + SCHEMA = { + vol.Exclusive(CONF_KNX_ROUTING, "connection_type"): ROUTING_SCHEMA, + vol.Exclusive(CONF_KNX_TUNNELING, "connection_type"): TUNNELING_SCHEMA, + vol.Optional( + CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS + ): ia_validator, + vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, + vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, + vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, + vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + } + ############# # PLATFORMS ############# -class BinarySensorSchema: +class KNXPlatformSchema(ABC): + """Voluptuous schema for KNX platform entity configuration.""" + + PLATFORM_NAME: ClassVar[str] + ENTITY_SCHEMA: ClassVar[vol.Schema] + + @classmethod + def platform_node(cls) -> dict[vol.Optional, vol.All]: + """Return a schema node for the platform.""" + return { + vol.Optional(cls.PLATFORM_NAME): vol.All( + cv.ensure_list, [cls.ENTITY_SCHEMA] + ) + } + + +class BinarySensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX binary sensors.""" + PLATFORM_NAME = SupportedPlatforms.BINARY_SENSOR.value + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE CONF_INVERT = CONF_INVERT @@ -116,7 +160,7 @@ class BinarySensorSchema: DEFAULT_NAME = "KNX Binary Sensor" - SCHEMA = vol.All( + ENTITY_SCHEMA = vol.All( # deprecated since September 2020 cv.deprecated("significant_bit"), cv.deprecated("automation"), @@ -137,9 +181,11 @@ class BinarySensorSchema: ) -class ClimateSchema: +class ClimateSchema(KNXPlatformSchema): """Voluptuous schema for KNX climate devices.""" + PLATFORM_NAME = SupportedPlatforms.CLIMATE.value + CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address" CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode" @@ -178,7 +224,7 @@ class ClimateSchema: DEFAULT_TEMPERATURE_STEP = 0.1 DEFAULT_ON_OFF_INVERT = False - SCHEMA = vol.All( + ENTITY_SCHEMA = vol.All( # deprecated since September 2020 cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP), # deprecated since 2021.6 @@ -245,9 +291,11 @@ class ClimateSchema: ) -class CoverSchema: +class CoverSchema(KNXPlatformSchema): """Voluptuous schema for KNX covers.""" + PLATFORM_NAME = SupportedPlatforms.COVER.value + CONF_MOVE_LONG_ADDRESS = "move_long_address" CONF_MOVE_SHORT_ADDRESS = "move_short_address" CONF_STOP_ADDRESS = "stop_address" @@ -263,7 +311,7 @@ class CoverSchema: DEFAULT_TRAVEL_TIME = 25 DEFAULT_NAME = "KNX Cover" - SCHEMA = vol.All( + ENTITY_SCHEMA = vol.All( vol.Schema( { vol.Required( @@ -297,9 +345,11 @@ class CoverSchema: ) -class ExposeSchema: +class ExposeSchema(KNXPlatformSchema): """Voluptuous schema for KNX exposures.""" + PLATFORM_NAME = CONF_KNX_EXPOSE + CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_BINARY = "binary" @@ -329,12 +379,14 @@ class ExposeSchema: vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, } ) - SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA) + ENTITY_SCHEMA = vol.Any(EXPOSE_SENSOR_SCHEMA, EXPOSE_TIME_SCHEMA) -class FanSchema: +class FanSchema(KNXPlatformSchema): """Voluptuous schema for KNX fans.""" + PLATFORM_NAME = SupportedPlatforms.FAN.value + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_OSCILLATION_ADDRESS = "oscillation_address" CONF_OSCILLATION_STATE_ADDRESS = "oscillation_state_address" @@ -342,7 +394,7 @@ class FanSchema: DEFAULT_NAME = "KNX Fan" - SCHEMA = vol.Schema( + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(KNX_ADDRESS): ga_list_validator, @@ -354,9 +406,11 @@ class FanSchema: ) -class LightSchema: +class LightSchema(KNXPlatformSchema): """Voluptuous schema for KNX lights.""" + PLATFORM_NAME = SupportedPlatforms.LIGHT.value + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_BRIGHTNESS_ADDRESS = "brightness_address" CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" @@ -390,7 +444,7 @@ class LightSchema: } ) - SCHEMA = vol.All( + ENTITY_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -452,12 +506,14 @@ class LightSchema: ) -class NotifySchema: +class NotifySchema(KNXPlatformSchema): """Voluptuous schema for KNX notifications.""" + PLATFORM_NAME = SupportedPlatforms.NOTIFY.value + DEFAULT_NAME = "KNX Notify" - SCHEMA = vol.Schema( + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(KNX_ADDRESS): ga_validator, @@ -465,13 +521,15 @@ class NotifySchema: ) -class SceneSchema: +class SceneSchema(KNXPlatformSchema): """Voluptuous schema for KNX scenes.""" + PLATFORM_NAME = SupportedPlatforms.SCENE.value + CONF_SCENE_NUMBER = "scene_number" DEFAULT_NAME = "KNX SCENE" - SCHEMA = vol.Schema( + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(KNX_ADDRESS): ga_list_validator, @@ -482,15 +540,17 @@ class SceneSchema: ) -class SensorSchema: +class SensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX sensors.""" + PLATFORM_NAME = SupportedPlatforms.SENSOR.value + CONF_ALWAYS_CALLBACK = "always_callback" CONF_STATE_ADDRESS = CONF_STATE_ADDRESS CONF_SYNC_STATE = CONF_SYNC_STATE DEFAULT_NAME = "KNX Sensor" - SCHEMA = vol.Schema( + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, @@ -501,14 +561,16 @@ class SensorSchema: ) -class SwitchSchema: +class SwitchSchema(KNXPlatformSchema): """Voluptuous schema for KNX switches.""" + PLATFORM_NAME = SupportedPlatforms.SWITCH.value + CONF_INVERT = CONF_INVERT CONF_STATE_ADDRESS = CONF_STATE_ADDRESS DEFAULT_NAME = "KNX Switch" - SCHEMA = vol.Schema( + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_INVERT, default=False): cv.boolean, @@ -518,9 +580,11 @@ class SwitchSchema: ) -class WeatherSchema: +class WeatherSchema(KNXPlatformSchema): """Voluptuous schema for KNX weather station.""" + PLATFORM_NAME = SupportedPlatforms.WEATHER.value + CONF_SYNC_STATE = CONF_SYNC_STATE CONF_KNX_TEMPERATURE_ADDRESS = "address_temperature" CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS = "address_brightness_south" @@ -538,7 +602,7 @@ class WeatherSchema: DEFAULT_NAME = "KNX Weather Station" - SCHEMA = vol.All( + ENTITY_SCHEMA = vol.All( # deprecated since 2021.6 cv.deprecated("create_sensors"), vol.Schema( From 549b0b07277c7d39647fa2c4852c6752ecf93758 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 1 Jun 2021 08:59:51 +0200 Subject: [PATCH 114/750] KNX: Support for XY-color lights (#51306) * support for xy-color * replace invalid name --- homeassistant/components/knx/light.py | 53 +++++++++++++++++--------- homeassistant/components/knx/schema.py | 4 ++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index ed4abac63b5..d8697a26d7f 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any, Tuple, cast from xknx import XKNX -from xknx.devices import Light as XknxLight +from xknx.devices.light import Light as XknxLight, XYYColor from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( @@ -12,11 +12,13 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, + ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_XY, LightEntity, ) from homeassistant.const import CONF_NAME @@ -159,6 +161,8 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS), group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS), group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS), + group_address_xyy_color=config.get(LightSchema.CONF_XYY_ADDRESS), + group_address_xyy_color_state=config.get(LightSchema.CONF_XYY_STATE_ADDRESS), group_address_tunable_white=group_address_tunable_white, group_address_tunable_white_state=group_address_tunable_white_state, group_address_color_temperature=group_address_color_temp, @@ -253,6 +257,9 @@ class KNXLight(KnxEntity, LightEntity): """Return the brightness of this light between 0..255.""" if self._device.supports_brightness: return self._device.current_brightness + if self._device.current_xyy_color is not None: + _, brightness = self._device.current_xyy_color + return brightness if (rgb := self.rgb_color) is not None: return max(rgb) return None @@ -277,6 +284,14 @@ class KNXLight(KnxEntity, LightEntity): return (*rgb, white) return None + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + if self._device.current_xyy_color is not None: + xy_color, _ = self._device.current_xyy_color + return xy_color + return None + @property def color_temp(self) -> int | None: """Return the color temperature in mireds.""" @@ -309,6 +324,8 @@ class KNXLight(KnxEntity, LightEntity): @property def color_mode(self) -> str | None: """Return the color mode of the light.""" + if self._device.supports_xyy_color: + return COLOR_MODE_XY if self._device.supports_rgbw: return COLOR_MODE_RGBW if self._device.supports_color: @@ -329,22 +346,11 @@ class KNXLight(KnxEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - # ignore arguments if not supported to fall back to set_on() - brightness = ( - kwargs.get(ATTR_BRIGHTNESS) - if self._device.supports_brightness - or self.color_mode in (COLOR_MODE_RGB, COLOR_MODE_RGBW) - else None - ) - mireds = ( - kwargs.get(ATTR_COLOR_TEMP) - if self.color_mode == COLOR_MODE_COLOR_TEMP - else None - ) - rgb = kwargs.get(ATTR_RGB_COLOR) if self.color_mode == COLOR_MODE_RGB else None - rgbw = ( - kwargs.get(ATTR_RGBW_COLOR) if self.color_mode == COLOR_MODE_RGBW else None - ) + brightness = kwargs.get(ATTR_BRIGHTNESS) + mireds = kwargs.get(ATTR_COLOR_TEMP) + rgb = kwargs.get(ATTR_RGB_COLOR) + rgbw = kwargs.get(ATTR_RGBW_COLOR) + xy_color = kwargs.get(ATTR_XY_COLOR) if ( not self.is_on @@ -352,6 +358,7 @@ class KNXLight(KnxEntity, LightEntity): and mireds is None and rgb is None and rgbw is None + and xy_color is None ): await self._device.set_on() return @@ -394,12 +401,22 @@ class KNXLight(KnxEntity, LightEntity): ) await self._device.set_tunable_white(relative_ct) + if xy_color is not None: + await self._device.set_xyy_color( + XYYColor(color=xy_color, brightness=brightness) + ) + return + if brightness is not None: # brightness: 1..255; 0 brightness will call async_turn_off() if self._device.brightness.writable: await self._device.set_brightness(brightness) return - # brightness without color in kwargs; set via color - default to white + # brightness without color in kwargs; set via color + if self.color_mode == COLOR_MODE_XY: + await self._device.set_xyy_color(XYYColor(brightness=brightness)) + return + # default to white if color not known for RGB(W) if self.color_mode == COLOR_MODE_RGBW: rgbw = self.rgbw_color if not rgbw or not any(rgbw): diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index fa94f503d7e..0715b68d575 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -421,6 +421,8 @@ class LightSchema(KNXPlatformSchema): CONF_COLOR_TEMP_MODE = "color_temperature_mode" CONF_RGBW_ADDRESS = "rgbw_address" CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" + CONF_XYY_ADDRESS = "xyy_address" + CONF_XYY_STATE_ADDRESS = "xyy_state_address" CONF_MIN_KELVIN = "min_kelvin" CONF_MAX_KELVIN = "max_kelvin" @@ -479,6 +481,8 @@ class LightSchema(KNXPlatformSchema): ): vol.All(vol.Upper, cv.enum(ColorTempModes)), vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator, vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator, + vol.Exclusive(CONF_XYY_ADDRESS, "color"): ga_list_validator, + vol.Optional(CONF_XYY_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), From 3c452f8c9b1d7950894c07080957ee2074675793 Mon Sep 17 00:00:00 2001 From: Daniel Rheinbay Date: Tue, 1 Jun 2021 09:04:49 +0200 Subject: [PATCH 115/750] Refactor yeelight integration to use only flows (#51255) * Refactor light.py to use only flows.py, eliminating transitions.py * Reformat yeelight source code using black --- homeassistant/components/yeelight/__init__.py | 24 +++------- .../components/yeelight/config_flow.py | 20 +++----- homeassistant/components/yeelight/light.py | 46 +++++-------------- tests/components/yeelight/__init__.py | 4 +- tests/components/yeelight/test_config_flow.py | 42 ++++------------- tests/components/yeelight/test_init.py | 39 +++------------- tests/components/yeelight/test_light.py | 23 +++------- 7 files changed, 46 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 8ed2164f75c..2cb754ce6a7 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -164,16 +164,11 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: # Import manually configured devices for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): _LOGGER.debug("Importing configured %s", host) - entry_config = { - CONF_HOST: host, - **device_config, - } + entry_config = {CONF_HOST: host, **device_config} hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=entry_config, - ), + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config + ) ) return True @@ -203,9 +198,7 @@ async def _async_initialize( entry.async_on_unload( async_dispatcher_connect( - hass, - DEVICE_INITIALIZED.format(host), - _async_load_platforms, + hass, DEVICE_INITIALIZED.format(host), _async_load_platforms ) ) @@ -224,10 +217,7 @@ def _async_populate_entry_options(hass: HomeAssistant, entry: ConfigEntry) -> No hass.config_entries.async_update_entry( entry, - data={ - CONF_HOST: entry.data.get(CONF_HOST), - CONF_ID: entry.data.get(CONF_ID), - }, + data={CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.data.get(CONF_ID)}, options={ CONF_NAME: entry.data.get(CONF_NAME, ""), CONF_MODEL: entry.data.get(CONF_MODEL, ""), @@ -613,9 +603,7 @@ class YeelightEntity(Entity): async def _async_get_device( - hass: HomeAssistant, - host: str, - entry: ConfigEntry, + hass: HomeAssistant, host: str, entry: ConfigEntry ) -> YeelightDevice: # Get model from config and capabilities model = entry.options.get(CONF_MODEL) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 0b0fe0d96c1..a66571cae93 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -83,10 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self._set_confirm_only() - placeholders = { - "model": self._discovered_model, - "host": self._discovered_ip, - } + placeholders = {"model": self._discovered_model, "host": self._discovered_ip} self.context["title_placeholders"] = placeholders return self.async_show_form( step_id="discovery_confirm", description_placeholders=placeholders @@ -105,8 +102,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: self._abort_if_unique_id_configured() return self.async_create_entry( - title=f"{model} {self.unique_id}", - data=user_input, + title=f"{model} {self.unique_id}", data=user_input ) user_input = user_input or {} @@ -126,8 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( - title=_async_unique_name(capabilities), - data={CONF_ID: unique_id}, + title=_async_unique_name(capabilities), data={CONF_ID: unique_id} ) configured_devices = { @@ -223,19 +218,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional(CONF_MODEL, default=options[CONF_MODEL]): str, vol.Required( - CONF_TRANSITION, - default=options[CONF_TRANSITION], + CONF_TRANSITION, default=options[CONF_TRANSITION] ): cv.positive_int, vol.Required( CONF_MODE_MUSIC, default=options[CONF_MODE_MUSIC] ): bool, vol.Required( - CONF_SAVE_ON_CHANGE, - default=options[CONF_SAVE_ON_CHANGE], + CONF_SAVE_ON_CHANGE, default=options[CONF_SAVE_ON_CHANGE] ): bool, vol.Required( - CONF_NIGHTLIGHT_SWITCH, - default=options[CONF_NIGHTLIGHT_SWITCH], + CONF_NIGHTLIGHT_SWITCH, default=options[CONF_NIGHTLIGHT_SWITCH] ): bool, } ), diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 0782ab94c61..6a4b7e85001 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -6,15 +6,7 @@ import logging import voluptuous as vol import yeelight -from yeelight import ( - Bulb, - BulbException, - Flow, - RGBTransition, - SleepTransition, - flows, - transitions as yee_transitions, -) +from yeelight import Bulb, BulbException, Flow, RGBTransition, SleepTransition, flows from yeelight.enums import BulbType, LightType, PowerMode, SceneClass from homeassistant.components.light import ( @@ -180,9 +172,7 @@ SERVICE_SCHEMA_SET_MODE = { vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) } -SERVICE_SCHEMA_SET_MUSIC_MODE = { - vol.Required(ATTR_MODE_MUSIC): cv.boolean, -} +SERVICE_SCHEMA_SET_MUSIC_MODE = {vol.Required(ATTR_MODE_MUSIC): cv.boolean} SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA @@ -358,11 +348,7 @@ def _async_setup_services(hass: HomeAssistant): transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), ) await hass.async_add_executor_job( - partial( - entity.set_scene, - SceneClass.CF, - flow, - ) + partial(entity.set_scene, SceneClass.CF, flow) ) async def _async_set_auto_delay_off_scene(entity, service_call): @@ -378,24 +364,16 @@ def _async_setup_services(hass: HomeAssistant): platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( - SERVICE_SET_MODE, - SERVICE_SCHEMA_SET_MODE, - "set_mode", + SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "set_mode" ) platform.async_register_entity_service( - SERVICE_START_FLOW, - SERVICE_SCHEMA_START_FLOW, - _async_start_flow, + SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow ) platform.async_register_entity_service( - SERVICE_SET_COLOR_SCENE, - SERVICE_SCHEMA_SET_COLOR_SCENE, - _async_set_color_scene, + SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene ) platform.async_register_entity_service( - SERVICE_SET_HSV_SCENE, - SERVICE_SCHEMA_SET_HSV_SCENE, - _async_set_hsv_scene, + SERVICE_SET_HSV_SCENE, SERVICE_SCHEMA_SET_HSV_SCENE, _async_set_hsv_scene ) platform.async_register_entity_service( SERVICE_SET_COLOR_TEMP_SCENE, @@ -413,9 +391,7 @@ def _async_setup_services(hass: HomeAssistant): _async_set_auto_delay_off_scene, ) platform.async_register_entity_service( - SERVICE_SET_MUSIC_MODE, - SERVICE_SCHEMA_SET_MUSIC_MODE, - "set_music_mode", + SERVICE_SET_MUSIC_MODE, SERVICE_SCHEMA_SET_MUSIC_MODE, "set_music_mode" ) @@ -707,11 +683,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): elif effect == EFFECT_FAST_RANDOM_LOOP: flow = flows.random_loop(duration=250) elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) + flow = flows.pulse(37, 211, 102, count=2) elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) + flow = flows.pulse(59, 89, 152, count=2) elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + flow = flows.pulse(0, 172, 237, count=2) else: return diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 38c28a9a900..d6306e273ff 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -74,9 +74,7 @@ YAML_CONFIGURATION = { } } -CONFIG_ENTRY_DATA = { - CONF_ID: ID, -} +CONFIG_ENTRY_DATA = {CONF_ID: ID} def _mocked_bulb(cannot_connect=False): diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index dd5ae85e89e..8994c8e3360 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -56,17 +56,13 @@ async def test_discovery(hass: HomeAssistant): assert not result["errors"] with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "form" assert result2["step_id"] == "pick_device" assert not result2["errors"] with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( - f"{MODULE}.async_setup_entry", - return_value=True, + f"{MODULE}.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} @@ -87,10 +83,7 @@ async def test_discovery(hass: HomeAssistant): assert not result["errors"] with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" @@ -102,10 +95,7 @@ async def test_discovery_no_device(hass: HomeAssistant): ) with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" @@ -138,8 +128,7 @@ async def test_import(hass: HomeAssistant): with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( f"{MODULE}.async_setup", return_value=True ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", - return_value=True, + f"{MODULE}.async_setup_entry", return_value=True ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config @@ -200,10 +189,7 @@ async def test_manual(hass: HomeAssistant): mocked_bulb = _mocked_bulb() with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( f"{MODULE}.async_setup", return_value=True - ), patch( - f"{MODULE}.async_setup_entry", - return_value=True, - ): + ), patch(f"{MODULE}.async_setup_entry", return_value=True): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -279,10 +265,7 @@ async def test_manual_no_capabilities(hass: HomeAssistant): type(mocked_bulb).get_capabilities = MagicMock(return_value=None) with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( f"{MODULE}.async_setup", return_value=True - ), patch( - f"{MODULE}.async_setup_entry", - return_value=True, - ): + ), patch(f"{MODULE}.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -354,16 +337,13 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): mocked_bulb = _mocked_bulb() with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": source}, - data=data, + DOMAIN, context={"source": source}, data=data ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( - f"{MODULE}.async_setup_entry", - return_value=True, + f"{MODULE}.async_setup_entry", return_value=True ) as mock_async_setup_entry: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "create_entry" @@ -393,9 +373,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data type(mocked_bulb).get_capabilities = MagicMock(return_value=None) with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": source}, - data=data, + DOMAIN, context={"source": source}, data=data ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 8a37c2b283e..2d1113d1896 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -45,12 +45,7 @@ from tests.common import MockConfigEntry async def test_ip_changes_fallback_discovery(hass: HomeAssistant): """Test Yeelight ip changes and we fallback to discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_ID: ID, - CONF_HOST: "5.5.5.5", - }, - unique_id=ID, + domain=DOMAIN, data={CONF_ID: ID, CONF_HOST: "5.5.5.5"}, unique_id=ID ) config_entry.add_to_hass(hass) @@ -60,12 +55,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): side_effect=[OSError, CAPABILITIES, CAPABILITIES] ) - _discovered_devices = [ - { - "capabilities": CAPABILITIES, - "ip": IP_ADDRESS, - } - ] + _discovered_devices = [{"capabilities": CAPABILITIES, "ip": IP_ADDRESS}] with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( f"{MODULE}.discover_bulbs", return_value=_discovered_devices ): @@ -92,12 +82,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): """Test Yeelight ip changes and we fallback to discovery.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "5.5.5.5", - }, - ) + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "5.5.5.5"}) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb(True) @@ -170,10 +155,7 @@ async def test_unique_ids_device(hass: HomeAssistant): """Test Yeelight unique IDs from yeelight device IDs.""" config_entry = MockConfigEntry( domain=DOMAIN, - data={ - **CONFIG_ENTRY_DATA, - CONF_NIGHTLIGHT_SWITCH: True, - }, + data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}, unique_id=ID, ) config_entry.add_to_hass(hass) @@ -197,11 +179,7 @@ async def test_unique_ids_device(hass: HomeAssistant): async def test_unique_ids_entry(hass: HomeAssistant): """Test Yeelight unique IDs from entry IDs.""" config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - **CONFIG_ENTRY_DATA, - CONF_NIGHTLIGHT_SWITCH: True, - }, + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} ) config_entry.add_to_hass(hass) @@ -231,12 +209,7 @@ async def test_unique_ids_entry(hass: HomeAssistant): async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): """Test Yeelight off while adding to ha, for example on HA start.""" config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - **CONFIG_ENTRY_DATA, - CONF_HOST: IP_ADDRESS, - }, - unique_id=ID, + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_HOST: IP_ADDRESS}, unique_id=ID ) config_entry.add_to_hass(hass) diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index b6ce2fa4faf..a001f099062 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -353,11 +353,7 @@ async def test_device_types(hass: HomeAssistant): entity_id=ENTITY_LIGHT, ): config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - **CONFIG_ENTRY_DATA, - CONF_NIGHTLIGHT_SWITCH: False, - }, + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) @@ -383,11 +379,7 @@ async def test_device_types(hass: HomeAssistant): if nightlight_properties is None: return config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - **CONFIG_ENTRY_DATA, - CONF_NIGHTLIGHT_SWITCH: True, - }, + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} ) config_entry.add_to_hass(hass) await _async_setup(config_entry) @@ -577,16 +569,13 @@ async def test_effects(hass: HomeAssistant): {YEELIGHT_SLEEP_TRANSACTION: [800]}, ], }, - }, - ], - }, + } + ] + } }, ) - config_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG_ENTRY_DATA, - ) + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() From 3d45f00fad2674268d049aee03fc52e0383c545f Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 1 Jun 2021 09:45:37 +0200 Subject: [PATCH 116/750] Bump aiopvpc to apply quickfix for new electricity price tariff (#51320) Since 2021-06-01, the three PVPC price tariffs become one and only: '2.0 TD', and the JSON schema from the official API (data source of this integration) is slightly different. This patch allows a no-pain jump between the old tariffs and the new one. --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index c39d66163e0..bbbe18350c8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.1.1"], + "requirements": ["aiopvpc==2.1.2"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 2054159d430..c40a65d8030 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9af5fb43134..1309c357b5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.1 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 From 6b0e57e641bc788c98515587e45d50399d62867f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 10:23:10 +0200 Subject: [PATCH 117/750] Define SwitchEntity entity attributes as class variables (#51232) --- homeassistant/components/switch/__init__.py | 40 +++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index c585fdc22d3..f468f6f6bd3 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -1,22 +1,27 @@ """Component to interface with switches that can be controlled remotely.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import final +from typing import Any, cast, final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -55,7 +60,7 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for switches.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -69,37 +74,41 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + return cast(bool, await hass.data[DOMAIN].async_setup_entry(entry)) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + return cast(bool, await hass.data[DOMAIN].async_unload_entry(entry)) class SwitchEntity(ToggleEntity): """Base class for switch entities.""" + _attr_current_power_w: float | None = None + _attr_is_standby: bool | None = None + _attr_today_energy_kwh: float | None = None + @property - def current_power_w(self): + def current_power_w(self) -> float | None: """Return the current power usage in W.""" - return None + return self._attr_current_power_w @property - def today_energy_kwh(self): + def today_energy_kwh(self) -> float | None: """Return the today total energy usage in kWh.""" - return None + return self._attr_today_energy_kwh @property - def is_standby(self): + def is_standby(self) -> bool | None: """Return true if device is in standby.""" - return None + return self._attr_is_standby @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" data = {} @@ -110,11 +119,6 @@ class SwitchEntity(ToggleEntity): return data - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return None - class SwitchDevice(SwitchEntity): """Representation of a switch (for backwards compatibility).""" From b91696c1392935d71986d96920f5b55821b9d063 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 1 Jun 2021 04:26:22 -0400 Subject: [PATCH 118/750] Switch to using entity class attributes where possible in zwave_js (#51207) * Switch to using entity class attributes where possible in zwave_js * fix * revert docstring * remove unused init * Revert some changes based on feedback in #51181 * switch to class atributes --- .../components/zwave_js/binary_sensor.py | 84 ++++++++----------- homeassistant/components/zwave_js/cover.py | 32 +++---- homeassistant/components/zwave_js/entity.py | 40 +++------ homeassistant/components/zwave_js/light.py | 13 ++- homeassistant/components/zwave_js/number.py | 8 +- homeassistant/components/zwave_js/sensor.py | 54 ++++++------ homeassistant/components/zwave_js/switch.py | 9 +- 7 files changed, 98 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index ad186b69fe4..537f4f8e49e 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -269,7 +269,20 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): ) -> None: """Initialize a ZWaveBooleanBinarySensor entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name(include_value_name=True) + + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + self._attr_device_class = ( + DEVICE_CLASS_BATTERY + if self.info.primary_value.command_class == CommandClass.BATTERY + else None + ) + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + self._attr_entity_registry_enabled_default = bool( + self.info.primary_value.command_class != CommandClass.SENSOR_BINARY + or self.info.node.device_class.generic.key == 0x20 + ) @property def is_on(self) -> bool | None: @@ -278,23 +291,6 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return None return bool(self.info.primary_value.value) - @property - def device_class(self) -> str | None: - """Return device class.""" - if self.info.primary_value.command_class == CommandClass.BATTERY: - return DEVICE_CLASS_BATTERY - return None - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - return bool( - self.info.primary_value.command_class != CommandClass.SENSOR_BINARY - or self.info.node.device_class.generic.key == 0x20 - ) - class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Representation of a Z-Wave binary_sensor from Notification CommandClass.""" @@ -309,13 +305,20 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): """Initialize a ZWaveNotificationBinarySensor entity.""" super().__init__(config_entry, client, info) self.state_key = state_key - self._name = self.generate_name( + # check if we have a custom mapping for this value + self._mapping_info = self._get_sensor_mapping() + + # Entity class attributes + self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, additional_info=[self.info.primary_value.metadata.states[self.state_key]], ) - # check if we have a custom mapping for this value - self._mapping_info = self._get_sensor_mapping() + self._attr_device_class = self._mapping_info.get("device_class") + self._attr_unique_id = f"{self._attr_unique_id}.{self.state_key}" + self._attr_entity_registry_enabled_default = ( + True if not self._mapping_info else self._mapping_info.get("enabled", True) + ) @property def is_on(self) -> bool | None: @@ -324,23 +327,6 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return None return int(self.info.primary_value.value) == int(self.state_key) - @property - def device_class(self) -> str | None: - """Return device class.""" - return self._mapping_info.get("device_class") - - @property - def unique_id(self) -> str: - """Return unique id for this entity.""" - return f"{super().unique_id}.{self.state_key}" - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - if not self._mapping_info: - return True - return self._mapping_info.get("enabled", True) - @callback def _get_sensor_mapping(self) -> NotificationSensorMapping: """Try to get a device specific mapping for this sensor.""" @@ -366,7 +352,15 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): super().__init__(config_entry, client, info) # check if we have a custom mapping for this value self._mapping_info = self._get_sensor_mapping() - self._name = self.generate_name(include_value_name=True) + + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + self._attr_device_class = self._mapping_info.get("device_class") + # We hide some more advanced sensors by default to not overwhelm users + # unless explicitly stated in a mapping, assume deisabled by default + self._attr_entity_registry_enabled_default = self._mapping_info.get( + "enabled", False + ) @property def is_on(self) -> bool | None: @@ -375,18 +369,6 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): return None return self.info.primary_value.value in self._mapping_info["on_states"] - @property - def device_class(self) -> str | None: - """Return device class.""" - return self._mapping_info.get("device_class") - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide some more advanced sensors by default to not overwhelm users - # unless explicitly stated in a mapping, assume deisabled by default - return self._mapping_info.get("enabled", False) - @callback def _get_sensor_mapping(self) -> PropertySensorMapping: """Try to get a device specific mapping for this sensor.""" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 2adc64d528f..e01f2871604 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -79,14 +79,21 @@ def percent_to_zwave_position(value: int) -> int: class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveCover entity.""" + super().__init__(config_entry, client, info) + + # Entity class attributes + self._attr_device_class = DEVICE_CLASS_WINDOW if self.info.platform_hint == "window_shutter": - return DEVICE_CLASS_SHUTTER + self._attr_device_class = DEVICE_CLASS_SHUTTER if self.info.platform_hint == "window_blind": - return DEVICE_CLASS_BLIND - return DEVICE_CLASS_WINDOW + self._attr_device_class = DEVICE_CLASS_BLIND @property def is_closed(self) -> bool | None: @@ -134,6 +141,9 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave motorized barrier device.""" + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + _attr_device_class = DEVICE_CLASS_GARAGE + def __init__( self, config_entry: ConfigEntry, @@ -146,16 +156,6 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): "targetState", add_to_watched_value_ids=False ) - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE - - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_GARAGE - @property def is_opening(self) -> bool | None: """Return if the cover is opening or not.""" diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 2d7dc961e68..548796911af 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -9,7 +9,7 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -30,10 +30,6 @@ class ZWaveBaseEntity(Entity): self.config_entry = config_entry self.client = client self.info = info - self._name = self.generate_name() - self._unique_id = get_unique_id( - self.client.driver.controller.home_id, self.info.primary_value.value_id - ) # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} @@ -42,6 +38,17 @@ class ZWaveBaseEntity(Entity): self.info.additional_value_ids_to_watch ) + # Entity class attributes + self._attr_name = self.generate_name() + self._attr_unique_id = get_unique_id( + self.client.driver.controller.home_id, self.info.primary_value.value_id + ) + self._attr_assumed_state = self.info.assumed_state + # device is precreated in main handler + self._attr_device_info = { + "identifiers": {get_device_id(self.client, self.info.node)}, + } + @callback def on_value_update(self) -> None: """Call when one of the watched values change. @@ -91,14 +98,6 @@ class ZWaveBaseEntity(Entity): ) ) - @property - def device_info(self) -> DeviceInfo: - """Return device information for the device registry.""" - # device is precreated in main handler - return { - "identifiers": {get_device_id(self.client, self.info.node)}, - } - def generate_name( self, include_value_name: bool = False, @@ -133,16 +132,6 @@ class ZWaveBaseEntity(Entity): return name - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" - return self._name - - @property - def unique_id(self) -> str: - """Return the unique_id of the entity.""" - return self._unique_id - @property def available(self) -> bool: """Return entity availability.""" @@ -229,8 +218,3 @@ class ZWaveBaseEntity(Entity): def should_poll(self) -> bool: """No polling needed.""" return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return self.info.assumed_state diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index a1ab78e6ee3..b50f2231f46 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -107,13 +107,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): value_property_key=ColorComponent.COLD_WHITE, ) self._supported_color_modes = set() - self._supported_features = 0 # get additional (optional) values and set features self._target_value = self.get_zwave_value("targetValue") self._dimming_duration = self.get_zwave_value("duration") - if self._dimming_duration is not None: - self._supported_features |= SUPPORT_TRANSITION self._calculate_color_values() if self._supports_rgbw: self._supported_color_modes.add(COLOR_MODE_RGBW) @@ -124,6 +121,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): if not self._supported_color_modes: self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + # Entity class attributes + self._attr_supported_features = 0 + if self._dimming_duration is not None: + self._attr_supported_features |= SUPPORT_TRANSITION + @callback def on_value_update(self) -> None: """Call when a watched value is added or updated.""" @@ -179,11 +181,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Flag supported features.""" return self._supported_color_modes - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" # RGB/HS color diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index f427f7fac20..808fd346be1 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -46,14 +46,16 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): ) -> None: """Initialize a ZwaveNumberEntity entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name( - include_value_name=True, alternate_value_name=info.platform_hint - ) if self.info.primary_value.metadata.writeable: self._target_value = self.info.primary_value else: self._target_value = self.get_zwave_value("targetValue") + # Entity class attributes + self._attr_name = self.generate_name( + include_value_name=True, alternate_value_name=info.platform_hint + ) + @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index b3c7db25116..064275e5729 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -101,9 +101,20 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): ) -> None: """Initialize a ZWaveSensorBase entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name(include_value_name=True) - self._device_class = self._get_device_class() - self._state_class = self._get_state_class() + + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + self._attr_device_class = self._get_device_class() + self._attr_state_class = self._get_state_class() + self._attr_entity_registry_enabled_default = True + # We hide some of the more advanced sensors by default to not overwhelm users + if self.info.primary_value.command_class in [ + CommandClass.BASIC, + CommandClass.CONFIGURATION, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + self._attr_entity_registry_enabled_default = False def _get_device_class(self) -> str | None: """ @@ -145,29 +156,6 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return STATE_CLASS_MEASUREMENT return None - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def state_class(self) -> str | None: - """Return the state class of the sensor.""" - return self._state_class - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide some of the more advanced sensors by default to not overwhelm users - if self.info.primary_value.command_class in [ - CommandClass.BASIC, - CommandClass.CONFIGURATION, - CommandClass.INDICATOR, - CommandClass.NOTIFICATION, - ]: - return False - return True - @property def force_update(self) -> bool: """Force updates.""" @@ -203,8 +191,10 @@ class ZWaveNumericSensor(ZwaveSensorBase): ) -> None: """Initialize a ZWaveNumericSensor entity.""" super().__init__(config_entry, client, info) + + # Entity class attributes if self.info.primary_value.command_class == CommandClass.BASIC: - self._name = self.generate_name( + self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.command_class_name, ) @@ -240,7 +230,9 @@ class ZWaveListSensor(ZwaveSensorBase): ) -> None: """Initialize a ZWaveListSensor entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name( + + # Entity class attributes + self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, additional_info=[self.info.primary_value.property_key_name], @@ -278,13 +270,15 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): ) -> None: """Initialize a ZWaveConfigParameterSensor entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name( + self._primary_value = cast(ConfigurationValue, self.info.primary_value) + + # Entity class attributes + self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, additional_info=[self.info.primary_value.property_key_name], name_suffix="Config Parameter", ) - self._primary_value = cast(ConfigurationValue, self.info.primary_value) @property def state(self) -> str | None: diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 0be5d1d7f61..1fb5480f2a1 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -88,21 +88,18 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): ) -> None: """Initialize a ZWaveBarrierEventSignalingSwitch entity.""" super().__init__(config_entry, client, info) - self._name = self.generate_name(include_value_name=True) self._state: bool | None = None self._update_state() + # Entity class attributes + self._attr_name = self.generate_name(include_value_name=True) + @callback def on_value_update(self) -> None: """Call when a watched value is added or updated.""" self._update_state() - @property - def name(self) -> str: - """Return default name from device name and value name combination.""" - return self._name - @property def is_on(self) -> bool | None: # type: ignore """Return a boolean for the state of the switch.""" From ed9b1993727a9a3967f5c667c8f0ad23b224e744 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Jun 2021 10:41:34 +0200 Subject: [PATCH 119/750] Fix exception after removing Shelly config entry and stopping HA (#51321) * Fix device shutdown twice * Change if logic --- homeassistant/components/shelly/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ab0facea920..4d7b8654720 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -288,8 +288,10 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): def shutdown(self): """Shutdown the wrapper.""" - self.device.shutdown() - self._async_remove_device_updates_handler() + if self.device: + self.device.shutdown() + self._async_remove_device_updates_handler() + self.device = None @callback def _handle_ha_stop(self, _): From 45e1473f83b98dab7af67406c6c55bb9c17c1bf8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 02:23:59 -0700 Subject: [PATCH 120/750] Improve config validation for key_value_schemas (#49429) --- homeassistant/helpers/config_validation.py | 16 ++++++++-------- tests/helpers/test_config_validation.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ed619cc9678..40457ef92c8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -770,25 +770,25 @@ def deprecated( def key_value_schemas( - key: str, value_schemas: dict[str, vol.Schema] -) -> Callable[[Any], dict[str, Any]]: + key: str, value_schemas: dict[Hashable, vol.Schema] +) -> Callable[[Any], dict[Hashable, Any]]: """Create a validator that validates based on a value for specific key. This gives better error messages. """ - def key_value_validator(value: Any) -> dict[str, Any]: + def key_value_validator(value: Any) -> dict[Hashable, Any]: if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") key_value = value.get(key) - if key_value not in value_schemas: - raise vol.Invalid( - f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(value_schemas)}" - ) + if isinstance(key_value, Hashable) and key_value in value_schemas: + return cast(Dict[Hashable, Any], value_schemas[key_value](value)) - return cast(Dict[str, Any], value_schemas[key_value](value)) + raise vol.Invalid( + f"Unexpected value for {key}: '{key_value}'. Expected {', '.join(str(key) for key in value_schemas)}" + ) return key_value_validator diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 232d7bbb8b6..02303825bbd 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1024,7 +1024,7 @@ def test_key_value_schemas(): schema(True) assert str(excinfo.value) == "Expected a dictionary" - for mode in None, "invalid": + for mode in None, {"a": "dict"}, "invalid": with pytest.raises(vol.Invalid) as excinfo: schema({"mode": mode}) assert ( From 94ae8396dd24059fb16cd15a0309c742d0a3239e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 12:38:49 +0200 Subject: [PATCH 121/750] Update frontend to 20210601.0 (#51329) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 49b51a7864c..0e0685cd772 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210531.1" + "home-assistant-frontend==20210601.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index abf13fa339e..50d46da672d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index c40a65d8030..cb069254e3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1309c357b5e..aa14355781c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210531.1 +home-assistant-frontend==20210601.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From bd0373388dd533a40fd38e374e698894f6c1eec5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 03:51:44 -0700 Subject: [PATCH 122/750] Trusted networks auth provider warns if detects a requests with x-forwarded-for header while the http integration is not configured for reverse proxies (#51319) * Trusted networks auth provider to require http integration configured for proxies to allow logging in with requests with x-forwarded-for header * Make it a warning --- .../auth/providers/trusted_networks.py | 111 +++++++++++++----- homeassistant/components/http/__init__.py | 1 + tests/auth/providers/test_trusted_networks.py | 74 ++++++++++-- 3 files changed, 146 insertions(+), 40 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 2f120e56652..e93587e91ca 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,10 +14,13 @@ from ipaddress import ( ip_address, ip_network, ) +import logging from typing import Any, Dict, List, Union, cast +from aiohttp import hdrs import voluptuous as vol +from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -86,40 +89,60 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False + @callback + def is_allowed_request(self) -> bool: + """Return if it is an allowed request.""" + request = http.current_request.get() + if request is not None and ( + self.hass.http.use_x_forwarded_for + or hdrs.X_FORWARDED_FOR not in request.headers + ): + return True + + logging.getLogger(__name__).warning( + "A request contained an x-forwarded-for header but your HTTP integration is not set-up " + "for reverse proxies. This usually means that you have not configured your reverse proxy " + "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this header." + ) + return True + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None + + if not self.is_allowed_request(): + return MisconfiguredTrustedNetworksLoginFlow(self) + ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ user for user in users if not user.system_generated and user.is_active ] for ip_net, user_or_group_list in self.trusted_users.items(): - if ip_addr in ip_net: - user_list = [ - user_id - for user_id in user_or_group_list - if isinstance(user_id, str) - ] - group_list = [ - group[CONF_GROUP] - for group in user_or_group_list - if isinstance(group, dict) - ] - flattened_group_list = [ - group for sublist in group_list for group in sublist - ] - available_users = [ - user - for user in available_users - if ( - user.id in user_list - or any( - group.id in flattened_group_list for group in user.groups - ) - ) - ] - break + if ip_addr not in ip_net: + continue + + user_list = [ + user_id for user_id in user_or_group_list if isinstance(user_id, str) + ] + group_list = [ + group[CONF_GROUP] + for group in user_or_group_list + if isinstance(group, dict) + ] + flattened_group_list = [ + group for sublist in group_list for group in sublist + ] + available_users = [ + user + for user in available_users + if ( + user.id in user_list + or any(group.id in flattened_group_list for group in user.groups) + ) + ] + break return TrustedNetworksLoginFlow( self, @@ -136,13 +159,22 @@ class TrustedNetworksAuthProvider(AuthProvider): users = await self.store.async_get_users() for user in users: - if not user.system_generated and user.is_active and user.id == user_id: - for credential in await self.async_credentials(): - if credential.data["user_id"] == user_id: - return credential - cred = self.async_create_credentials({"user_id": user_id}) - await self.store.async_link_user(user, cred) - return cred + if user.id != user_id: + continue + + if user.system_generated: + continue + + if not user.is_active: + continue + + for credential in await self.async_credentials(): + if credential.data["user_id"] == user_id: + return credential + + cred = self.async_create_credentials({"user_id": user_id}) + await self.store.async_link_user(user, cred) + return cred # We only allow login as exist user raise InvalidUserError @@ -163,6 +195,11 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ + if not self.is_allowed_request(): + raise InvalidAuthError( + "No request or it contains x-forwarded-for header and that's not allowed by configuration" + ) + if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -183,6 +220,16 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) +class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): + """Login handler for misconfigured trusted networks.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the step of the form.""" + return self.async_abort(reason="forwared_for_header_not_allowed") + + class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8bd20e31628..db198cb334a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -252,6 +252,7 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 412f660adc3..cceb3d720c0 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,10 +5,13 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth +from homeassistant import auth, const from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.setup import async_setup_component + +FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -112,7 +115,17 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -async def test_trusted_networks_credentials(manager, provider): +@pytest.fixture +def mock_allowed_request(): + """Mock that the request is allowed.""" + with patch( + "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", + return_value=True, + ): + yield + + +async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -129,7 +142,7 @@ async def test_trusted_networks_credentials(manager, provider): await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider): +async def test_validate_access(provider, mock_allowed_request): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -144,7 +157,7 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider): +async def test_validate_refresh_token(provider, mock_allowed_request): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -154,7 +167,7 @@ async def test_validate_refresh_token(provider): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider): +async def test_login_flow(manager, provider, mock_allowed_request): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -181,7 +194,9 @@ async def test_login_flow(manager, provider): assert step["data"]["user"] == user.id -async def test_trusted_users_login(manager_with_user, provider_with_user): +async def test_trusted_users_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -261,7 +276,9 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): assert schema({"user": sys_user.id}) -async def test_trusted_group_login(manager_with_user, provider_with_user): +async def test_trusted_group_login( + manager_with_user, provider_with_user, mock_allowed_request +): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -314,7 +331,9 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): assert schema({"user": user.id}) -async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): +async def test_bypass_login_flow( + manager_bypass_login, provider_bypass_login, mock_allowed_request +): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -345,3 +364,42 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) + + +async def test_allowed_request(hass, provider, current_request, caplog): + """Test allowing requests.""" + assert await async_setup_component(hass, "http", {}) + + provider.async_validate_access(ip_address("192.168.0.1")) + + current_request.get.return_value = current_request.get.return_value.clone( + headers={ + **current_request.get.return_value.headers, + "x-forwarded-for": "1.2.3.4", + } + ) + + if FORWARD_FOR_IS_WARNING: + caplog.clear() + provider.async_validate_access(ip_address("192.168.0.1")) + assert "This request will be blocked" in caplog.text + else: + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.0.1")) + + hass.http.use_x_forwarded_for = True + + provider.async_validate_access(ip_address("192.168.0.1")) + + +@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") +async def test_login_flow_no_request(provider): + """Test getting a login flow.""" + login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) + assert await login_flow.async_step_init() == { + "description_placeholders": None, + "flow_id": None, + "handler": None, + "reason": "forwared_for_header_not_allowed", + "type": "abort", + } From f3715cef6d36a3999b1745ac473dc94bada3b449 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 1 Jun 2021 14:48:53 +0300 Subject: [PATCH 123/750] Bump aioswitcher to 1.2.3 (#51324) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index a5af1187f07..84527954a2d 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,6 +3,6 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi","@thecode"], - "requirements": ["aioswitcher==1.2.1"], + "requirements": ["aioswitcher==1.2.3"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index cb069254e3e..7893f8a056e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -230,7 +230,7 @@ aiorecollect==1.0.4 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==1.2.1 +aioswitcher==1.2.3 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa14355781c..6405bea95b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aiorecollect==1.0.4 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==1.2.1 +aioswitcher==1.2.3 # homeassistant.components.syncthing aiosyncthing==0.5.1 From fb281c6bdecf3d93b016b52bbb5d0682046c70a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Jun 2021 15:09:23 +0200 Subject: [PATCH 124/750] Add arch to payload (#51330) --- homeassistant/components/analytics/analytics.py | 2 ++ homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index e6e7ffac337..571ffd90f22 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -21,6 +21,7 @@ from .const import ( ANALYTICS_ENDPOINT_URL_DEV, ATTR_ADDON_COUNT, ATTR_ADDONS, + ATTR_ARCH, ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, @@ -157,6 +158,7 @@ class Analytics: payload[ATTR_SUPERVISOR] = { ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + ATTR_ARCH: supervisor_info[ATTR_ARCH], } if operating_system_info.get(ATTR_BOARD) is not None: diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 16929a7131d..4688c578a00 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -16,6 +16,7 @@ LOGGER: logging.Logger = logging.getLogger(__package__) ATTR_ADDON_COUNT = "addon_count" ATTR_ADDONS = "addons" +ATTR_ARCH = "arch" ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 09f82e37fba..ee67a7e3935 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -132,7 +132,9 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"supported": True, "healthy": True}), + side_effect=Mock( + return_value={"supported": True, "healthy": True, "arch": "amd64"} + ), ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={"board": "blue", "version": "123"}), @@ -157,7 +159,10 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{MOCK_VERSION}'" in caplog.text - assert "'supervisor': {'healthy': True, 'supported': True}" in caplog.text + assert ( + "'supervisor': {'healthy': True, 'supported': True, 'arch': 'amd64'}" + in caplog.text + ) assert "'operating_system': {'board': 'blue', 'version': '123'}" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -197,6 +202,7 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), @@ -303,6 +309,7 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), From 63e16de6c0f305f6bb7ec1b81c31e0e68163af3e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Jun 2021 17:07:45 +0200 Subject: [PATCH 125/750] Improve time condition trace (#51335) --- homeassistant/helpers/condition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a59ad459874..23816b94a65 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -756,15 +756,18 @@ def time( ) if after < before: + condition_trace_update_result(after=after, now_time=now_time, before=before) if not after <= now_time < before: return False else: + condition_trace_update_result(after=after, now_time=now_time, before=before) if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] + condition_trace_update_result(weekday=weekday, now_weekday=now_weekday) if ( isinstance(weekday, str) and weekday != now_weekday From c5dc99c05211a144ec57f1b9509c9b08e6993fef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 17:57:23 +0200 Subject: [PATCH 126/750] Fix time condition microsecond offset when using input helpers (#51337) --- homeassistant/helpers/condition.py | 1 - tests/helpers/test_condition.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 23816b94a65..a467d952683 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -752,7 +752,6 @@ def time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), before_entity.attributes.get("second", 59), - 999999, ) if after < before: diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 9347d0bc025..2290ce9f679 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -791,6 +791,34 @@ async def test_time_using_input_datetime(hass): hass, after="input_datetime.pm", before="input_datetime.am" ) + # Trigger on PM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=18, minute=0, second=0), + ): + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.pm") + assert not condition.time(hass, before="input_datetime.pm") + + # Trigger on AM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=6, minute=0, second=0), + ): + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.am") + assert not condition.time(hass, before="input_datetime.am") + with pytest.raises(ConditionError): condition.time(hass, after="input_datetime.not_existing") From d975f9eb0ac0b3d84d4c1c1d300dc8e6e6c39f75 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 1 Jun 2021 17:58:25 +0200 Subject: [PATCH 127/750] Fix Netatmo sensor logic (#51338) --- homeassistant/components/netatmo/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ed75ddf2f7f..2dbbeb56c76 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: platform_not_ready = False async_add_entities(await find_entities(data_class_name), True) @@ -228,7 +228,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) data_class = data_handler.data.get(signal_name) - if not (data_class and data_class.raw_data): + if data_class and data_class.raw_data: nonlocal platform_not_ready platform_not_ready = False From cdd1f6b2f0cd6a934e17fa84f53283c88ecc7ce2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 18:38:55 +0200 Subject: [PATCH 128/750] Always load middle to handle forwarded proxy data (#51332) --- .../auth/providers/trusted_networks.py | 40 ---------- homeassistant/components/http/__init__.py | 6 +- homeassistant/components/http/forwarded.py | 36 +++++++-- tests/auth/providers/test_trusted_networks.py | 74 ++----------------- tests/components/http/test_auth.py | 2 +- tests/components/http/test_forwarded.py | 63 +++++++++++----- 6 files changed, 85 insertions(+), 136 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index e93587e91ca..fd2014667f8 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,13 +14,10 @@ from ipaddress import ( ip_address, ip_network, ) -import logging from typing import Any, Dict, List, Union, cast -from aiohttp import hdrs import voluptuous as vol -from homeassistant.components import http from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -89,31 +86,9 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - @callback - def is_allowed_request(self) -> bool: - """Return if it is an allowed request.""" - request = http.current_request.get() - if request is not None and ( - self.hass.http.use_x_forwarded_for - or hdrs.X_FORWARDED_FOR not in request.headers - ): - return True - - logging.getLogger(__name__).warning( - "A request contained an x-forwarded-for header but your HTTP integration is not set-up " - "for reverse proxies. This usually means that you have not configured your reverse proxy " - "correctly. This request will be blocked in Home Assistant 2021.7 unless you configure " - "your HTTP integration to allow this header." - ) - return True - async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None - - if not self.is_allowed_request(): - return MisconfiguredTrustedNetworksLoginFlow(self) - ip_addr = cast(IPAddress, context.get("ip_address")) users = await self.store.async_get_users() available_users = [ @@ -195,11 +170,6 @@ class TrustedNetworksAuthProvider(AuthProvider): Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ - if not self.is_allowed_request(): - raise InvalidAuthError( - "No request or it contains x-forwarded-for header and that's not allowed by configuration" - ) - if not self.trusted_networks: raise InvalidAuthError("trusted_networks is not configured") @@ -220,16 +190,6 @@ class TrustedNetworksAuthProvider(AuthProvider): self.async_validate_access(ip_address(remote_ip)) -class MisconfiguredTrustedNetworksLoginFlow(LoginFlow): - """Login handler for misconfigured trusted networks.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: - """Handle the step of the form.""" - return self.async_abort(reason="forwared_for_header_not_allowed") - - class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index db198cb334a..19e3437b79c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -232,10 +232,7 @@ class HomeAssistantHTTP: # forwarded middleware needs to go second. setup_security_filter(app) - # Only register middleware if `use_x_forwarded_for` is enabled - # and trusted proxies are provided - if use_x_forwarded_for and trusted_proxies: - async_setup_forwarded(app, trusted_proxies) + async_setup_forwarded(app, use_x_forwarded_for, trusted_proxies) setup_request_context(app, current_request) @@ -252,7 +249,6 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port - self.use_x_forwarded_for = use_x_forwarded_for self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 5c5726a2597..5c62a469924 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -14,7 +14,9 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: +def async_setup_forwarded( + app: Application, use_x_forwarded_for: bool | None, trusted_proxies: list[str] +) -> None: """Create forwarded middleware for the app. Process IP addresses, proto and host information in the forwarded for headers. @@ -73,15 +75,37 @@ def async_setup_forwarded(app: Application, trusted_proxies: list[str]) -> None: # No forwarding headers, continue as normal return await handler(request) - # Ensure the IP of the connected peer is trusted - assert request.transport is not None + # Get connected IP + if ( + request.transport is None + or request.transport.get_extra_info("peername") is None + ): + # Connected IP isn't retrieveable from the request transport, continue + return await handler(request) + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) - if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + + # We have X-Forwarded-For, but config does not agree + if not use_x_forwarded_for: _LOGGER.warning( - "Received X-Forwarded-For header from untrusted proxy %s, headers not processed", + "A request from a reverse proxy was received from %s, but your " + "HTTP integration is not set-up for reverse proxies; " + "This request will be blocked in Home Assistant 2021.7 unless " + "you configure your HTTP integration to allow this header", connected_ip, ) - # Not trusted, continue as normal + # Block this request in the future, for now we pass. + return await handler(request) + + # Ensure the IP of the connected peer is trusted + if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + _LOGGER.warning( + "Received X-Forwarded-For header from untrusted proxy %s, headers not processed; " + "This request will be blocked in Home Assistant 2021.7 unless you configure " + "your HTTP integration to allow this proxy to reverse your Home Assistant instance", + connected_ip, + ) + # Not trusted, Block this request in the future, continue as normal return await handler(request) # Multiple X-Forwarded-For headers diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index cceb3d720c0..412f660adc3 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -5,13 +5,10 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant import auth, const +from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY -from homeassistant.setup import async_setup_component - -FORWARD_FOR_IS_WARNING = (const.MAJOR_VERSION, const.MINOR_VERSION) < (2021, 8) @pytest.fixture @@ -115,17 +112,7 @@ def manager_bypass_login(hass, store, provider_bypass_login): ) -@pytest.fixture -def mock_allowed_request(): - """Mock that the request is allowed.""" - with patch( - "homeassistant.auth.providers.trusted_networks.TrustedNetworksAuthProvider.is_allowed_request", - return_value=True, - ): - yield - - -async def test_trusted_networks_credentials(manager, provider, mock_allowed_request): +async def test_trusted_networks_credentials(manager, provider): """Test trusted_networks credentials related functions.""" owner = await manager.async_create_user("test-owner") tn_owner_cred = await provider.async_get_or_create_credentials({"user": owner.id}) @@ -142,7 +129,7 @@ async def test_trusted_networks_credentials(manager, provider, mock_allowed_requ await provider.async_get_or_create_credentials({"user": "invalid-user"}) -async def test_validate_access(provider, mock_allowed_request): +async def test_validate_access(provider): """Test validate access from trusted networks.""" provider.async_validate_access(ip_address("192.168.0.1")) provider.async_validate_access(ip_address("192.168.128.10")) @@ -157,7 +144,7 @@ async def test_validate_access(provider, mock_allowed_request): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) -async def test_validate_refresh_token(provider, mock_allowed_request): +async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: with pytest.raises(tn_auth.InvalidAuthError): @@ -167,7 +154,7 @@ async def test_validate_refresh_token(provider, mock_allowed_request): mock.assert_called_once_with(ip_address("127.0.0.1")) -async def test_login_flow(manager, provider, mock_allowed_request): +async def test_login_flow(manager, provider): """Test login flow.""" owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") @@ -194,9 +181,7 @@ async def test_login_flow(manager, provider, mock_allowed_request): assert step["data"]["user"] == user.id -async def test_trusted_users_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_users_login(manager_with_user, provider_with_user): """Test available user list changed per different IP.""" owner = await manager_with_user.async_create_user("test-owner") sys_user = await manager_with_user.async_create_system_user( @@ -276,9 +261,7 @@ async def test_trusted_users_login( assert schema({"user": sys_user.id}) -async def test_trusted_group_login( - manager_with_user, provider_with_user, mock_allowed_request -): +async def test_trusted_group_login(manager_with_user, provider_with_user): """Test config trusted_user with group_id.""" owner = await manager_with_user.async_create_user("test-owner") # create a user in user group @@ -331,9 +314,7 @@ async def test_trusted_group_login( assert schema({"user": user.id}) -async def test_bypass_login_flow( - manager_bypass_login, provider_bypass_login, mock_allowed_request -): +async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): """Test login flow can be bypass if only one user available.""" owner = await manager_bypass_login.async_create_user("test-owner") @@ -364,42 +345,3 @@ async def test_bypass_login_flow( # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) - - -async def test_allowed_request(hass, provider, current_request, caplog): - """Test allowing requests.""" - assert await async_setup_component(hass, "http", {}) - - provider.async_validate_access(ip_address("192.168.0.1")) - - current_request.get.return_value = current_request.get.return_value.clone( - headers={ - **current_request.get.return_value.headers, - "x-forwarded-for": "1.2.3.4", - } - ) - - if FORWARD_FOR_IS_WARNING: - caplog.clear() - provider.async_validate_access(ip_address("192.168.0.1")) - assert "This request will be blocked" in caplog.text - else: - with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access(ip_address("192.168.0.1")) - - hass.http.use_x_forwarded_for = True - - provider.async_validate_access(ip_address("192.168.0.1")) - - -@pytest.mark.skipif(FORWARD_FOR_IS_WARNING, reason="Currently a warning") -async def test_login_flow_no_request(provider): - """Test getting a login flow.""" - login_flow = await provider.async_login_flow({"ip_address": ip_address("1.1.1.1")}) - assert await login_flow.async_step_init() == { - "description_placeholders": None, - "flow_id": None, - "handler": None, - "reason": "forwared_for_header_not_allowed", - "type": "abort", - } diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 71c01630a67..6bd1d622b12 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -53,7 +53,7 @@ def app(hass): app = web.Application() app["hass"] = hass app.router.add_get("/", mock_handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) return app diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 2946c0b383c..4b7a3421b0a 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -28,7 +28,7 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -74,7 +74,7 @@ async def test_x_forwarded_for_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) async_setup_forwarded( - app, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] ) mock_api_client = await aiohttp_client(app) @@ -83,6 +83,33 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 +async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): + """Test that we warn when processing is disabled, but proxy has been detected.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, False, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + assert ( + "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " + "integration is not set-up for reverse proxies" in caplog.text + ) + + async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): """Test that we get the IP from transport with untrusted proxy.""" @@ -97,7 +124,7 @@ async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("1.1.1.1")]) + async_setup_forwarded(app, True, [ip_network("1.1.1.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -119,7 +146,7 @@ async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -148,7 +175,7 @@ async def test_x_forwarded_for_with_malformed_header( """Test that we get a HTTP 400 bad request with a malformed header.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -162,7 +189,7 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -193,7 +220,7 @@ async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -245,7 +272,7 @@ async def test_x_forwarded_proto_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -273,7 +300,7 @@ async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client) app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -301,7 +328,7 @@ async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) @@ -313,7 +340,7 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -339,7 +366,7 @@ async def test_x_forwarded_proto_empty_element( """Test that we get a HTTP 400 bad request with empty proto.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -364,7 +391,7 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( """Test that we get a HTTP 400 bad request with incorrect number of elements.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -397,7 +424,7 @@ async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -421,7 +448,7 @@ async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -446,7 +473,7 @@ async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) @@ -458,7 +485,7 @@ async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -478,7 +505,7 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with empty host value.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( From 6a474e74e827ad5c38ba3113c81ac8dfefdfeb95 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Tue, 1 Jun 2021 20:07:51 +0300 Subject: [PATCH 129/750] Fix Snapcast state after restoring snapshot (#51340) --- homeassistant/components/snapcast/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index dcb4b62b35a..26bf4c903a6 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -198,6 +198,7 @@ class SnapcastGroupDevice(MediaPlayerEntity): async def async_restore(self): """Restore the group state.""" await self._group.restore() + self.async_write_ha_state() class SnapcastClientDevice(MediaPlayerEntity): @@ -326,6 +327,7 @@ class SnapcastClientDevice(MediaPlayerEntity): async def async_restore(self): """Restore the client state.""" await self._client.restore() + self.async_write_ha_state() async def async_set_latency(self, latency): """Set the latency of the client.""" From 91b6f9d7d09e093e9bbec2fe16617eb89e728ebf Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 1 Jun 2021 19:26:54 +0200 Subject: [PATCH 130/750] Bump zwave-js-server-python to 0.26.0 (#51341) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index c68206373ba..5ce65fcbb35 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.25.1"], + "requirements": ["zwave-js-server-python==0.26.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7893f8a056e..3a027685f62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2441,4 +2441,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6405bea95b1..956dc0b37e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1323,4 +1323,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.25.1 +zwave-js-server-python==0.26.0 From e5dff49440ac303a6b66392044acf6098c0f104b Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 1 Jun 2021 20:32:17 +0200 Subject: [PATCH 131/750] Fix SIA event data func (#51339) --- homeassistant/components/sia/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 08e0fce8ab2..66fdd7d95be 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -30,7 +30,7 @@ def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create a dict from the SIA Event for the HA Event.""" return { - "message_type": event.message_type, + "message_type": event.message_type.value, "receiver": event.receiver, "line": event.line, "account": event.account, @@ -43,7 +43,7 @@ def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: "message": event.message, "x_data": event.x_data, "timestamp": event.timestamp.isoformat(), - "event_qualifier": event.qualifier, + "event_qualifier": event.event_qualifier, "event_type": event.event_type, "partition": event.partition, "extended_data": [ From fcdd8b11a6364d826a0535f6ff75f796ce4fa8d1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 21:43:55 +0200 Subject: [PATCH 132/750] Collection of changing entity properties to class attributes - 2 (#51345) --- homeassistant/components/atome/sensor.py | 14 +----- .../components/braviatv/media_player.py | 7 +-- .../components/flick_electric/sensor.py | 7 +-- .../components/greeneye_monitor/sensor.py | 47 +++++-------------- homeassistant/components/gtfs/sensor.py | 7 +-- .../components/homekit_controller/__init__.py | 10 +--- .../homekit_controller/humidifier.py | 14 ++---- .../homekit_controller/media_player.py | 7 +-- .../components/homematic/binary_sensor.py | 5 +- homeassistant/components/icloud/sensor.py | 13 ++--- homeassistant/components/ipp/sensor.py | 7 +-- .../components/islamic_prayer_times/sensor.py | 19 ++------ homeassistant/components/lightwave/sensor.py | 13 ++--- .../components/linode/binary_sensor.py | 7 +-- homeassistant/components/myq/binary_sensor.py | 7 +-- homeassistant/components/nextbus/sensor.py | 17 ++----- .../components/nuki/binary_sensor.py | 7 +-- homeassistant/components/oru/sensor.py | 13 ++--- .../components/philips_js/media_player.py | 7 +-- homeassistant/components/skybeacon/sensor.py | 14 ++---- homeassistant/components/smarthab/cover.py | 7 +-- .../components/uk_transport/sensor.py | 17 ++----- homeassistant/components/unifi/sensor.py | 12 ++--- .../components/withings/binary_sensor.py | 7 +-- 24 files changed, 70 insertions(+), 215 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 285b6c70713..f47dd4033b9 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -39,8 +39,6 @@ WEEKLY_TYPE = "week" MONTHLY_TYPE = "month" YEARLY_TYPE = "year" -ICON = "mdi:flash" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, @@ -217,6 +215,8 @@ class AtomeData: class AtomeSensor(SensorEntity): """Representation of a sensor entity for Atome.""" + _attr_device_class = DEVICE_CLASS_POWER + def __init__(self, data, name, sensor_type): """Initialize the sensor.""" self._name = name @@ -251,16 +251,6 @@ class AtomeSensor(SensorEntity): """Return the unit of measurement.""" return self._unit_of_measurement - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_POWER - def update(self): """Update device state.""" update_function = getattr(self._data, f"update_{self._sensor_type}_usage") diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 14b47f95101..90ad562c0ed 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -121,6 +121,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BraviaTVDevice(MediaPlayerEntity): """Representation of a Bravia TV.""" + _attr_device_class = DEVICE_CLASS_TV + def __init__(self, client, name, pin, unique_id, device_info, ignored_sources): """Initialize the Bravia TV device.""" @@ -238,11 +240,6 @@ class BraviaTVDevice(MediaPlayerEntity): """Return the name of the device.""" return self._name - @property - def device_class(self): - """Set the device class to TV.""" - return DEVICE_CLASS_TV - @property def unique_id(self): """Return a unique_id for this entity.""" diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index c523271716a..ab628e205c7 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -36,6 +36,8 @@ async def async_setup_entry( class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" + _attr_unit_of_measurement = UNIT_NAME + def __init__(self, api: FlickAPI) -> None: """Entity object for Flick Electric sensor.""" self._api: FlickAPI = api @@ -55,11 +57,6 @@ class FlickPricingSensor(SensorEntity): """Return the state of the sensor.""" return self._price.price - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return UNIT_NAME - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 4e792bf56e4..337a471eb2b 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -88,6 +88,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class GEMSensor(SensorEntity): """Base class for GreenEye Monitor sensors.""" + _attr_should_poll = False + def __init__(self, monitor_serial_number, name, sensor_type, number): """Construct the entity.""" self._monitor_serial_number = monitor_serial_number @@ -96,11 +98,6 @@ class GEMSensor(SensorEntity): self._sensor_type = sensor_type self._number = number - @property - def should_poll(self): - """GEM pushes changes, so this returns False.""" - return False - @property def unique_id(self): """Return a unique ID for this sensor.""" @@ -148,6 +145,9 @@ class GEMSensor(SensorEntity): class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" + _attr_icon = CURRENT_SENSOR_ICON + _attr_unit_of_measurement = UNIT_WATTS + def __init__(self, monitor_serial_number, number, name, net_metering): """Construct the entity.""" super().__init__(monitor_serial_number, name, "current", number) @@ -156,16 +156,6 @@ class CurrentSensor(GEMSensor): def _get_sensor(self, monitor): return monitor.channels[self._number - 1] - @property - def icon(self): - """Return the icon that should represent this sensor in the UI.""" - return CURRENT_SENSOR_ICON - - @property - def unit_of_measurement(self): - """Return the unit of measurement used by this sensor.""" - return UNIT_WATTS - @property def state(self): """Return the current number of watts being used by the channel.""" @@ -191,6 +181,8 @@ class CurrentSensor(GEMSensor): class PulseCounter(GEMSensor): """Entity showing rate of change in one pulse counter of the monitor.""" + _attr_icon = COUNTER_ICON + def __init__( self, monitor_serial_number, @@ -209,11 +201,6 @@ class PulseCounter(GEMSensor): def _get_sensor(self, monitor): return monitor.pulse_counters[self._number - 1] - @property - def icon(self): - """Return the icon that should represent this sensor in the UI.""" - return COUNTER_ICON - @property def state(self): """Return the current rate of change for the given pulse counter.""" @@ -253,6 +240,8 @@ class PulseCounter(GEMSensor): class TemperatureSensor(GEMSensor): """Entity showing temperature from one temperature sensor.""" + _attr_icon = TEMPERATURE_ICON + def __init__(self, monitor_serial_number, number, name, unit): """Construct the entity.""" super().__init__(monitor_serial_number, name, "temp", number) @@ -261,11 +250,6 @@ class TemperatureSensor(GEMSensor): def _get_sensor(self, monitor): return monitor.temperature_sensors[self._number - 1] - @property - def icon(self): - """Return the icon that should represent this sensor in the UI.""" - return TEMPERATURE_ICON - @property def state(self): """Return the current temperature being reported by this sensor.""" @@ -283,6 +267,9 @@ class TemperatureSensor(GEMSensor): class VoltageSensor(GEMSensor): """Entity showing voltage.""" + _attr_icon = VOLTAGE_ICON + _attr_unit_of_measurement = VOLT + def __init__(self, monitor_serial_number, number, name): """Construct the entity.""" super().__init__(monitor_serial_number, name, "volts", number) @@ -291,11 +278,6 @@ class VoltageSensor(GEMSensor): """Wire the updates to the monitor itself, since there is no voltage element in the API.""" return monitor - @property - def icon(self): - """Return the icon that should represent this sensor in the UI.""" - return VOLTAGE_ICON - @property def state(self): """Return the current voltage being reported by this sensor.""" @@ -303,8 +285,3 @@ class VoltageSensor(GEMSensor): return None return self._sensor.voltage - - @property - def unit_of_measurement(self): - """Return the unit of measurement for this sensor.""" - return VOLT diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index d71a2fab67d..812e6a58f28 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -518,6 +518,8 @@ def setup_platform( class GTFSDepartureSensor(SensorEntity): """Implementation of a GTFS departure sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__( self, gtfs: Any, @@ -576,11 +578,6 @@ class GTFSDepartureSensor(SensorEntity): """Icon to use in the frontend, if any.""" return self._icon - @property - def device_class(self) -> str: - """Return the class of this device.""" - return DEVICE_CLASS_TIMESTAMP - def update(self) -> None: """Get the latest data from GTFS and update the states.""" with self.lock: diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 3db6c1800c9..44d8286984c 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -32,6 +32,8 @@ def escape_characteristic_name(char_name): class HomeKitEntity(Entity): """Representation of a Home Assistant HomeKit device.""" + _attr_should_poll = False + def __init__(self, accessory, devinfo): """Initialise a generic HomeKit device.""" self._accessory = accessory @@ -99,14 +101,6 @@ class HomeKitEntity(Entity): payload = self.service.build_update(characteristics) return await self._accessory.put_characteristics(payload) - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from HKDevice. - """ - return False - def setup(self): """Configure an entity baed on its HomeKit characteristics metadata.""" self.pollable_characteristics = [] diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 227174d00e9..dfddd29f2ff 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -35,6 +35,8 @@ HA_MODE_TO_HK = { class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" + _attr_device_class = DEVICE_CLASS_HUMIDIFIER + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -45,11 +47,6 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD, ] - @property - def device_class(self) -> str: - """Return the device class of the device.""" - return DEVICE_CLASS_HUMIDIFIER - @property def supported_features(self): """Return the list of supported features.""" @@ -140,6 +137,8 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): """Representation of a HomeKit Controller Humidifier.""" + _attr_device_class = DEVICE_CLASS_DEHUMIDIFIER + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -151,11 +150,6 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD, ] - @property - def device_class(self) -> str: - """Return the device class of the device.""" - return DEVICE_CLASS_DEHUMIDIFIER - @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 71bde5f0af9..1134e4bb4da 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -57,6 +57,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): """Representation of a HomeKit Controller Television.""" + _attr_device_class = DEVICE_CLASS_TV + def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -70,11 +72,6 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): CharacteristicsTypes.IDENTIFIER, ] - @property - def device_class(self): - """Define the device class for a HomeKit enabled TV.""" - return DEVICE_CLASS_TV - @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 286c7372fd2..c57e4cd15c7 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -74,10 +74,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity): class HMBatterySensor(HMDevice, BinarySensorEntity): """Representation of an HomeMatic low battery sensor.""" - @property - def device_class(self): - """Return battery as a device class.""" - return DEVICE_CLASS_BATTERY + _attr_device_class = DEVICE_CLASS_BATTERY @property def is_on(self): diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 0e1bda16d60..ec55a1fcedd 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -53,6 +53,9 @@ def add_entities(account, async_add_entities, tracked): class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, account: IcloudAccount, device: IcloudDevice) -> None: """Initialize the battery sensor.""" self._account = account @@ -69,21 +72,11 @@ class IcloudDeviceBatterySensor(SensorEntity): """Sensor name.""" return f"{self._device.name} battery state" - @property - def device_class(self) -> str: - """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY - @property def state(self) -> int: """Battery state percentage.""" return self._device.battery_level - @property - def unit_of_measurement(self) -> str: - """Battery state measured in percentage.""" - return PERCENTAGE - @property def icon(self) -> str: """Battery state icon handling.""" diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 0d6dbdff065..da56ada41f2 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -184,6 +184,8 @@ class IPPPrinterSensor(IPPSensor): class IPPUptimeSensor(IPPSensor): """Defines a IPP uptime sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__( self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator ) -> None: @@ -203,8 +205,3 @@ class IPPUptimeSensor(IPPSensor): """Return the state of the sensor.""" uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() - - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 3133320d978..2fa563785d2 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -23,6 +23,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class IslamicPrayerTimeSensor(SensorEntity): """Representation of an Islamic prayer time sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_icon = PRAYER_TIMES_ICON + _attr_should_poll = False + def __init__(self, sensor_type, client): """Initialize the Islamic prayer time sensor.""" self.sensor_type = sensor_type @@ -38,11 +42,6 @@ class IslamicPrayerTimeSensor(SensorEntity): """Return the unique id of the entity.""" return self.sensor_type - @property - def icon(self): - """Icon to display in the front end.""" - return PRAYER_TIMES_ICON - @property def state(self): """Return the state of the sensor.""" @@ -52,16 +51,6 @@ class IslamicPrayerTimeSensor(SensorEntity): .isoformat() ) - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index 1128078f8bc..f1b6412ab6a 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -25,6 +25,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class LightwaveBattery(SensorEntity): """Lightwave TRV Battery.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, name, lwlink, serial): """Initialize the Lightwave Trv battery sensor.""" self._name = name @@ -32,11 +35,6 @@ class LightwaveBattery(SensorEntity): self._lwlink = lwlink self._serial = serial - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_BATTERY - @property def name(self): """Return the name of the sensor.""" @@ -47,11 +45,6 @@ class LightwaveBattery(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def unit_of_measurement(self): - """Return the state of the sensor.""" - return PERCENTAGE - def update(self): """Communicate with a Lightwave RTF Proxy to get state.""" (dummy_temp, dummy_targ, battery, dummy_output) = self._lwlink.read_trv_status( diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 70a15eaf4e0..6769d72594b 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -50,6 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LinodeBinarySensor(BinarySensorEntity): """Representation of a Linode droplet sensor.""" + _attr_device_class = DEVICE_CLASS_MOVING + def __init__(self, li, node_id): """Initialize a new Linode sensor.""" self._linode = li @@ -69,11 +71,6 @@ class LinodeBinarySensor(BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_MOVING - @property def extra_state_attributes(self): """Return the state attributes of the Linode Node.""" diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index b1b3680343d..96ab589253b 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -32,16 +32,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY + def __init__(self, coordinator, device): """Initialize with API object, device id.""" super().__init__(coordinator) self._device = device - @property - def device_class(self): - """We track connectivity for gateways.""" - return DEVICE_CLASS_CONNECTIVITY - @property def name(self): """Return the name of the garage door if any.""" diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 67d0a4a81d7..fb03bcd25b5 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -18,8 +18,6 @@ CONF_AGENCY = "agency" CONF_ROUTE = "route" CONF_STOP = "stop" -ICON = "mdi:bus" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_AGENCY): cv.string, @@ -114,6 +112,9 @@ class NextBusDepartureSensor(SensorEntity): the future using fuzzy logic and matching. """ + _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_icon = "mdi:bus" + def __init__(self, client, agency, route, stop, name=None): """Initialize sensor with all required config.""" self.agency = agency @@ -144,11 +145,6 @@ class NextBusDepartureSensor(SensorEntity): return self._name - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - @property def state(self): """Return current state of the sensor.""" @@ -159,13 +155,6 @@ class NextBusDepartureSensor(SensorEntity): """Return additional state attributes.""" return self._attributes - @property - def icon(self): - """Return icon to be used for this sensor.""" - # Would be nice if we could determine if the line is a train or bus - # however that doesn't seem to be available to us. Using bus for now. - return ICON - def update(self): """Update sensor with new departures times.""" # Note: using Multi because there is a bug with the single stop impl diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 37641dbf15a..3b79eef324f 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -29,6 +29,8 @@ async def async_setup_entry(hass, entry, async_add_entities): class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" + _attr_device_class = DEVICE_CLASS_DOOR + @property def name(self): """Return the name of the lock.""" @@ -66,8 +68,3 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): def is_on(self): """Return true if the door is open.""" return self.door_sensor_state == STATE_DOORSENSOR_OPENED - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_DOOR diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index 063c0c6169d..c17873aefea 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -41,6 +41,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class CurrentEnergyUsageSensor(SensorEntity): """Representation of the sensor.""" + _attr_icon = SENSOR_ICON + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + def __init__(self, meter): """Initialize the sensor.""" self._state = None @@ -57,21 +60,11 @@ class CurrentEnergyUsageSensor(SensorEntity): """Return the name of the sensor.""" return SENSOR_NAME - @property - def icon(self): - """Return the icon of the sensor.""" - return SENSOR_ICON - @property def state(self): """Return the state of the sensor.""" return self._state - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ENERGY_KILO_WATT_HOUR - def update(self): """Fetch new state data for the sensor.""" try: diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 61aa97a66b1..e4512fc52f0 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -123,6 +123,8 @@ async def async_setup_entry( class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" + _attr_device_class = DEVICE_CLASS_TV + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -315,11 +317,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): if app: return app.get("label") - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TV - @property def unique_id(self): """Return unique identifier if known.""" diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 3fdd2e55b0d..fd707f9dd96 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -64,6 +64,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SkybeaconHumid(SensorEntity): """Representation of a Skybeacon humidity sensor.""" + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, name, mon): """Initialize a sensor.""" self.mon = mon @@ -79,11 +81,6 @@ class SkybeaconHumid(SensorEntity): """Return the state of the device.""" return self.mon.data["humid"] - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -93,6 +90,8 @@ class SkybeaconHumid(SensorEntity): class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" + _attr_unit_of_measurement = TEMP_CELSIUS + def __init__(self, name, mon): """Initialize a sensor.""" self.mon = mon @@ -108,11 +107,6 @@ class SkybeaconTemp(SensorEntity): """Return the state of the device.""" return self.mon.data["temp"] - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return TEMP_CELSIUS - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 4fc663fc3d8..64e693941b5 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -37,6 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SmartHabCover(CoverEntity): """Representation a cover.""" + _attr_device_class = DEVICE_CLASS_WINDOW + def __init__(self, cover): """Initialize a SmartHabCover.""" self._cover = cover @@ -69,11 +71,6 @@ class SmartHabCover(CoverEntity): """Return if the cover is closed or not.""" return self._cover.state == 0 - @property - def device_class(self) -> str: - """Return the class of this device, from component DEVICE_CLASSES.""" - return DEVICE_CLASS_WINDOW - async def async_open_cover(self, **kwargs): """Open the cover.""" await self._cover.async_open() diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index a0448230dd1..41549c202b3 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -92,7 +92,8 @@ class UkTransportSensor(SensorEntity): """ TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/" - ICON = "mdi:train" + _attr_icon = "mdi:train" + _attr_unit_of_measurement = TIME_MINUTES def __init__(self, name, api_app_id, api_app_key, url): """Initialize the sensor.""" @@ -113,16 +114,6 @@ class UkTransportSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return TIME_MINUTES - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self.ICON - def _do_api_request(self, params): """Perform an API request.""" request_params = dict( @@ -144,7 +135,7 @@ class UkTransportSensor(SensorEntity): class UkTransportLiveBusTimeSensor(UkTransportSensor): """Live bus time sensor from UK transportapi.com.""" - ICON = "mdi:bus" + _attr_icon = "mdi:bus" def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval): """Construct a live bus time sensor.""" @@ -206,7 +197,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): class UkTransportLiveTrainTimeSensor(UkTransportSensor): """Live train time sensor from UK transportapi.com.""" - ICON = "mdi:train" + _attr_icon = "mdi:train" def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval): """Construct a live bus time sensor.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index c8238602856..14456bb8c06 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -86,16 +86,13 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN + _attr_unit_of_measurement = DATA_MEGABYTES + @property def name(self) -> str: """Return the name of the client.""" return f"{super().name} {self.TYPE.upper()}" - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return DATA_MEGABYTES - async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_allow_bandwidth_sensors: @@ -134,10 +131,7 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN TYPE = UPTIME_SENSOR - @property - def device_class(self) -> str: - """Return device class.""" - return DEVICE_CLASS_TIMESTAMP + _attr_device_class = DEVICE_CLASS_TIMESTAMP @property def name(self) -> str: diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index ecb52530d7e..25ed695e8ff 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -29,12 +29,9 @@ async def async_setup_entry( class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): """Implementation of a Withings sensor.""" + _attr_device_class = DEVICE_CLASS_OCCUPANCY + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._state_data - - @property - def device_class(self) -> str: - """Provide the device class.""" - return DEVICE_CLASS_OCCUPANCY From 12b8672f84dbff3111b19b34dcb172517bedde36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 1 Jun 2021 22:21:07 +0200 Subject: [PATCH 133/750] Use entity class vars for Melcloud (#51351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/melcloud/climate.py | 69 ++++---------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 42c5ed7ef85..98aeaf73be1 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -100,11 +100,12 @@ async def async_setup_entry( class MelCloudClimate(ClimateEntity): """Base climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, device: MelCloudDevice) -> None: """Initialize the climate.""" self.api = device self._base_device = self.api.device - self._name = device.name async def async_update(self): """Update state from MELCloud.""" @@ -124,20 +125,17 @@ class MelCloudClimate(ClimateEntity): class AtaDeviceClimate(MelCloudClimate): """Air-to-Air climate device.""" + _attr_supported_features = ( + SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_SWING_MODE + ) + def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None: """Initialize the climate.""" super().__init__(device) self._device = ata_device - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{self.api.device.serial}-{self.api.device.mac}" - - @property - def name(self): - """Return the display name of this entity.""" - return self._name + self._attr_name = device.name + self._attr_unique_id = f"{self.api.device.serial}-{self.api.device.mac}" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -163,11 +161,6 @@ class AtaDeviceClimate(MelCloudClimate): ) return attr - @property - def temperature_unit(self) -> str: - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -258,11 +251,6 @@ class AtaDeviceClimate(MelCloudClimate): """Return a list of available vertical vane positions and modes.""" return self._device.vane_vertical_positions - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_SWING_MODE - async def async_turn_on(self) -> None: """Turn the entity on.""" await self._device.set({"power": True}) @@ -293,6 +281,10 @@ class AtaDeviceClimate(MelCloudClimate): class AtwDeviceZoneClimate(MelCloudClimate): """Air-to-Water zone climate device.""" + _attr_max_temp = 30 + _attr_min_temp = 10 + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE + def __init__( self, device: MelCloudDevice, atw_device: AtwDevice, atw_zone: Zone ) -> None: @@ -301,15 +293,8 @@ class AtwDeviceZoneClimate(MelCloudClimate): self._device = atw_device self._zone = atw_zone - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{self.api.device.serial}-{self._zone.zone_index}" - - @property - def name(self) -> str: - """Return the display name of this entity.""" - return f"{self._name} {self._zone.name}" + self._attr_name = f"{device.name} {self._zone.name}" + self._attr_unique_id = f"{self.api.device.serial}-{atw_zone.zone_index}" @property def extra_state_attributes(self) -> dict[str, Any]: @@ -321,11 +306,6 @@ class AtwDeviceZoneClimate(MelCloudClimate): } return data - @property - def temperature_unit(self) -> str: - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -372,24 +352,3 @@ class AtwDeviceZoneClimate(MelCloudClimate): await self._zone.set_target_temperature( kwargs.get("temperature", self.target_temperature) ) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def min_temp(self) -> float: - """Return the minimum temperature. - - MELCloud API does not expose radiator zone temperature limits. - """ - return 10 - - @property - def max_temp(self) -> float: - """Return the maximum temperature. - - MELCloud API does not expose radiator zone temperature limits. - """ - return 30 From 9e3ee73b8b8f351fc0895dc117ddf78bfda7552f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Jun 2021 15:28:56 -0500 Subject: [PATCH 134/750] Handle incomplete Sonos alarm event payloads (#51353) --- homeassistant/components/sonos/speaker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0d7efae1877..3932b6d3364 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -372,7 +372,8 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: """Create a task to update alarms from an event.""" - update_id = event.variables["alarm_list_version"] + if not (update_id := event.variables.get("alarm_list_version")): + return if update_id in self.processed_alarm_events: return self.processed_alarm_events.append(update_id) From ee2c950716d810b33fa244438044ca0332ea3a54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Jun 2021 13:34:31 -0700 Subject: [PATCH 135/750] Merge system options into pref properties (#51347) * Make system options future proof * Update tests * Add types --- .../components/config/config_entries.py | 82 ++++++------- homeassistant/config_entries.py | 99 ++++++++-------- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- homeassistant/helpers/update_coordinator.py | 2 +- tests/common.py | 6 +- .../bmw_connected_drive/test_config_flow.py | 1 - .../components/config/test_config_entries.py | 95 ++++++--------- .../forked_daapd/test_config_flow.py | 1 - .../forked_daapd/test_media_player.py | 1 - .../components/home_plus_control/conftest.py | 1 - .../homekit_controller/test_storage.py | 1 - .../components/homematicip_cloud/conftest.py | 1 - tests/components/hue/conftest.py | 1 - tests/components/hue/test_bridge.py | 4 - tests/components/hue/test_light.py | 1 - tests/components/huisbaasje/test_init.py | 3 - tests/components/huisbaasje/test_sensor.py | 2 - .../hvv_departures/test_config_flow.py | 3 - tests/components/smartthings/conftest.py | 1 - tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_switch.py | 2 - tests/components/zwave/test_lock.py | 1 - tests/helpers/test_entity_platform.py | 4 +- tests/helpers/test_entity_registry.py | 6 +- tests/helpers/test_update_coordinator.py | 2 +- tests/test_config_entries.py | 108 ++++++++++-------- 27 files changed, 188 insertions(+), 245 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 9d88a9b5311..7fe5cb0d190 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,6 @@ """Http views to control the config manager.""" +from __future__ import annotations + import aiohttp.web_exceptions import voluptuous as vol @@ -7,7 +9,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, @@ -31,7 +33,6 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command(config_entry_disable) hass.components.websocket_api.async_register_command(config_entry_update) hass.components.websocket_api.async_register_command(config_entries_progress) - hass.components.websocket_api.async_register_command(system_options_update) hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -230,14 +231,21 @@ def config_entries_progress(hass, connection, msg): ) -def send_entry_not_found(connection, msg_id): +def send_entry_not_found( + connection: websocket_api.ActiveConnection, msg_id: int +) -> None: """Send Config entry not found error.""" connection.send_error( msg_id, websocket_api.const.ERR_NOT_FOUND, "Config entry not found" ) -def get_entry(hass, connection, entry_id, msg_id): +def get_entry( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + entry_id: str, + msg_id: int, +) -> config_entries.ConfigEntry | None: """Get entry, send error message if it doesn't exist.""" entry = hass.config_entries.async_get_entry(entry_id) if entry is None: @@ -249,49 +257,13 @@ def get_entry(hass, connection, entry_id, msg_id): @websocket_api.async_response @websocket_api.websocket_command( { - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": str, - vol.Optional("disable_new_entities"): bool, - vol.Optional("disable_polling"): bool, + vol.Optional("title"): str, + vol.Optional("pref_disable_new_entities"): bool, + vol.Optional("pref_disable_polling"): bool, } ) -async def system_options_update(hass, connection, msg): - """Update config entry system options.""" - changes = dict(msg) - changes.pop("id") - changes.pop("type") - changes.pop("entry_id") - - entry = get_entry(hass, connection, msg["entry_id"], msg["id"]) - if entry is None: - return - - old_disable_polling = entry.system_options.disable_polling - - hass.config_entries.async_update_entry(entry, system_options=changes) - - result = { - "system_options": entry.system_options.as_dict(), - "require_restart": False, - } - - if ( - old_disable_polling != entry.system_options.disable_polling - and entry.state is config_entries.ConfigEntryState.LOADED - ): - if not await hass.config_entries.async_reload(entry.entry_id): - result["require_restart"] = ( - entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD - ) - - connection.send_result(msg["id"], result) - - -@websocket_api.require_admin -@websocket_api.async_response -@websocket_api.websocket_command( - {"type": "config_entries/update", "entry_id": str, vol.Optional("title"): str} -) async def config_entry_update(hass, connection, msg): """Update config entry.""" changes = dict(msg) @@ -303,8 +275,25 @@ async def config_entry_update(hass, connection, msg): if entry is None: return + old_disable_polling = entry.pref_disable_polling + hass.config_entries.async_update_entry(entry, **changes) - connection.send_result(msg["id"], entry_json(entry)) + + result = { + "config_entry": entry_json(entry), + "require_restart": False, + } + + if ( + old_disable_polling != entry.pref_disable_polling + and entry.state is config_entries.ConfigEntryState.LOADED + ): + if not await hass.config_entries.async_reload(entry.entry_id): + result["require_restart"] = ( + entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD + ) + + connection.send_result(msg["id"], result) @websocket_api.require_admin @@ -391,7 +380,8 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state.value, "supports_options": supports_options, "supports_unload": entry.supports_unload, - "system_options": entry.system_options.as_dict(), + "pref_disable_new_entities": entry.pref_disable_new_entities, + "pref_disable_polling": entry.pref_disable_polling, "disabled_by": entry.disabled_by, "reason": entry.reason, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 33c18fc0d7c..eeaf0149cc2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,8 +11,6 @@ from types import MappingProxyType, MethodType from typing import Any, Callable, Optional, cast import weakref -import attr - from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback @@ -152,7 +150,8 @@ class ConfigEntry: "options", "unique_id", "supports_unload", - "system_options", + "pref_disable_new_entities", + "pref_disable_polling", "source", "state", "disabled_by", @@ -170,7 +169,8 @@ class ConfigEntry: title: str, data: Mapping[str, Any], source: str, - system_options: dict, + pref_disable_new_entities: bool | None = None, + pref_disable_polling: bool | None = None, options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, @@ -197,7 +197,15 @@ class ConfigEntry: self.options = MappingProxyType(options or {}) # Entry system options - self.system_options = SystemOptions(**system_options) + if pref_disable_new_entities is None: + pref_disable_new_entities = False + + self.pref_disable_new_entities = pref_disable_new_entities + + if pref_disable_polling is None: + pref_disable_polling = False + + self.pref_disable_polling = pref_disable_polling # Source of the configuration (user, discovery, cloud) self.source = source @@ -535,7 +543,8 @@ class ConfigEntry: "title": self.title, "data": dict(self.data), "options": dict(self.options), - "system_options": self.system_options.as_dict(), + "pref_disable_new_entities": self.pref_disable_new_entities, + "pref_disable_polling": self.pref_disable_polling, "source": self.source, "unique_id": self.unique_id, "disabled_by": self.disabled_by, @@ -652,7 +661,6 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): title=result["title"], data=result["data"], options=result["options"], - system_options={}, source=flow.context["source"], unique_id=flow.unique_id, ) @@ -845,8 +853,18 @@ class ConfigEntries: self._entries = {} return - self._entries = { - entry["entry_id"]: ConfigEntry( + entries = {} + + for entry in config["entries"]: + pref_disable_new_entities = entry.get("pref_disable_new_entities") + + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + if pref_disable_new_entities is None and "system_options" in entry: + pref_disable_new_entities = entry.get("system_options", {}).get( + "disable_new_entities" + ) + + entries[entry["entry_id"]] = ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], @@ -855,15 +873,16 @@ class ConfigEntries: title=entry["title"], # New in 0.89 options=entry.get("options"), - # New in 0.98 - system_options=entry.get("system_options", {}), # New in 0.104 unique_id=entry.get("unique_id"), # New in 2021.3 disabled_by=entry.get("disabled_by"), + # New in 2021.6 + pref_disable_new_entities=pref_disable_new_entities, + pref_disable_polling=entry.get("pref_disable_polling"), ) - for entry in config["entries"] - } + + self._entries = entries async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -962,11 +981,12 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: str | dict | None | UndefinedType = UNDEFINED, - title: str | dict | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, - system_options: dict | UndefinedType = UNDEFINED, + pref_disable_new_entities: bool | UndefinedType = UNDEFINED, + pref_disable_polling: bool | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -978,13 +998,17 @@ class ConfigEntries: """ changed = False - if unique_id is not UNDEFINED and entry.unique_id != unique_id: - changed = True - entry.unique_id = cast(Optional[str], unique_id) + for attr, value in ( + ("unique_id", unique_id), + ("title", title), + ("pref_disable_new_entities", pref_disable_new_entities), + ("pref_disable_polling", pref_disable_polling), + ): + if value == UNDEFINED or getattr(entry, attr) == value: + continue - if title is not UNDEFINED and entry.title != title: + setattr(entry, attr, value) changed = True - entry.title = cast(str, title) if data is not UNDEFINED and entry.data != data: # type: ignore changed = True @@ -994,11 +1018,6 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if system_options is not UNDEFINED: - old_system_options = entry.system_options.as_dict() - entry.system_options.update(**system_options) - changed = entry.system_options.as_dict() != old_system_options - if not changed: return False @@ -1401,34 +1420,6 @@ class OptionsFlow(data_entry_flow.FlowHandler): handler: str -@attr.s(slots=True) -class SystemOptions: - """Config entry system options.""" - - disable_new_entities: bool = attr.ib(default=False) - disable_polling: bool = attr.ib(default=False) - - def update( - self, - *, - disable_new_entities: bool | UndefinedType = UNDEFINED, - disable_polling: bool | UndefinedType = UNDEFINED, - ) -> None: - """Update properties.""" - if disable_new_entities is not UNDEFINED: - self.disable_new_entities = disable_new_entities - - if disable_polling is not UNDEFINED: - self.disable_polling = disable_polling - - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this config entries system options.""" - return { - "disable_new_entities": self.disable_new_entities, - "disable_polling": self.disable_polling, - } - - class EntityRegistryDisabledHandler: """Handler to handle when entities related to config entries updating disabled_by.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f0d691a1c8d..b22fb9ec2d2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -397,7 +397,7 @@ class EntityPlatform: raise if ( - (self.config_entry and self.config_entry.system_options.disable_polling) + (self.config_entry and self.config_entry.pref_disable_polling) or self._async_unsub_polling is not None or not any(entity.should_poll for entity in self.entities.values()) ): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index dbb3fae0e53..fc9ef575c7d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -286,7 +286,7 @@ class EntityRegistry: if ( disabled_by is None and config_entry - and config_entry.system_options.disable_new_entities + and config_entry.pref_disable_new_entities ): disabled_by = DISABLED_INTEGRATION diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index e91acfaf82f..e83a2d0edc3 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -111,7 +111,7 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return - if self.config_entry and self.config_entry.system_options.disable_polling: + if self.config_entry and self.config_entry.pref_disable_polling: return if self._unsub_refresh: diff --git a/tests/common.py b/tests/common.py index 952350fe68c..03b53294db0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -732,7 +732,8 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, - system_options={}, + pref_disable_new_entities=None, + pref_disable_polling=None, unique_id=None, disabled_by=None, reason=None, @@ -742,7 +743,8 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid_util.random_uuid_hex(), "domain": domain, "data": data or {}, - "system_options": system_options, + "pref_disable_new_entities": pref_disable_new_entities, + "pref_disable_polling": pref_disable_polling, "options": options, "version": version, "title": title, diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 75ca9b4aa1c..6a0bd210387 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -29,7 +29,6 @@ FIXTURE_CONFIG_ENTRY = { CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, - "system_options": {"disable_new_entities": False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 570d847e86e..0e1b471cbd5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -87,10 +87,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": None, }, @@ -101,10 +99,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", }, @@ -115,10 +111,8 @@ async def test_get_entries(hass, client): "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -340,10 +334,8 @@ async def test_create_account(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "Test Entry", "reason": None, }, @@ -415,10 +407,8 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, - "system_options": { - "disable_new_entities": False, - "disable_polling": False, - }, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "user-title", "reason": None, }, @@ -698,7 +688,7 @@ async def test_two_step_options_flow(hass, client): } -async def test_update_system_options(hass, hass_ws_client): +async def test_update_prefrences(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -706,64 +696,45 @@ async def test_update_system_options(hass, hass_ws_client): entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is False - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": entry.entry_id, - "disable_new_entities": True, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == { - "require_restart": False, - "system_options": {"disable_new_entities": True, "disable_polling": False}, - } - assert entry.system_options.disable_new_entities is True - assert entry.system_options.disable_polling is False + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is False await ws_client.send_json( { "id": 6, - "type": "config_entries/system_options/update", + "type": "config_entries/update", "entry_id": entry.entry_id, - "disable_new_entities": False, - "disable_polling": True, + "pref_disable_new_entities": True, } ) response = await ws_client.receive_json() assert response["success"] - assert response["result"] == { - "require_restart": True, - "system_options": {"disable_new_entities": False, "disable_polling": True}, - } - assert entry.system_options.disable_new_entities is False - assert entry.system_options.disable_polling is True + assert response["result"]["require_restart"] is False + assert response["result"]["config_entry"]["pref_disable_new_entities"] is True + assert response["result"]["config_entry"]["pref_disable_polling"] is False - -async def test_update_system_options_nonexisting(hass, hass_ws_client): - """Test that we can update entry.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False await ws_client.send_json( { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": "non_existing", - "disable_new_entities": True, + "id": 7, + "type": "config_entries/update", + "entry_id": entry.entry_id, + "pref_disable_new_entities": False, + "pref_disable_polling": True, } ) response = await ws_client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "not_found" + assert response["success"] + assert response["result"]["require_restart"] is True + assert response["result"]["config_entry"]["pref_disable_new_entities"] is False + assert response["result"]["config_entry"]["pref_disable_polling"] is True + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry(hass, hass_ws_client): @@ -785,7 +756,7 @@ async def test_update_entry(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["title"] == "Updated Title" + assert response["result"]["config_entry"]["title"] == "Updated Title" assert entry.title == "Updated Title" diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index a99f91d3f91..668f1be0a4f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -46,7 +46,6 @@ def config_entry_fixture(): title="", data=data, options={}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 032e3dde22c..a2e0050c3d9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -282,7 +282,6 @@ def config_entry_fixture(): title="", data=data, options={CONF_TTS_PAUSE_TIME: 0}, - system_options={}, source=SOURCE_USER, entry_id=1, ) diff --git a/tests/components/home_plus_control/conftest.py b/tests/components/home_plus_control/conftest.py index 4b60f2623c4..78a0da41fb8 100644 --- a/tests/components/home_plus_control/conftest.py +++ b/tests/components/home_plus_control/conftest.py @@ -35,7 +35,6 @@ def mock_config_entry(): }, source="test", options={}, - system_options={"disable_new_entities": False}, unique_id=DOMAIN, entry_id="home_plus_control_entry_id", ) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b1c3ee9ff4c..aa0a5e55057 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -95,7 +95,6 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): "TestData", pairing_data, "test", - system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b5dd6105e0f..c720df4a1bb 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -62,7 +62,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: unique_id=HAPID, data=entry_data, source=SOURCE_IMPORT, - system_options={"disable_new_entities": False}, ) return config_entry diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index b5c2aec3042..648337d7539 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -127,7 +127,6 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): domain=hue.DOMAIN, title="Mock Title", data={"host": hostname}, - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index eb5c93862fe..034acf88efa 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -181,7 +181,6 @@ async def test_hue_activate_scene(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -215,7 +214,6 @@ async def test_hue_activate_scene_transition(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -249,7 +247,6 @@ async def test_hue_activate_scene_group_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -278,7 +275,6 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 5efb74d015f..f4f663c23ae 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,7 +179,6 @@ async def setup_bridge(hass, mock_bridge): "Mock Title", {"host": "mock-host"}, "test", - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index dde62a9c78b..390dc6c304d 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -41,7 +41,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -81,7 +80,6 @@ async def test_setup_entry_error(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -122,7 +120,6 @@ async def test_unload_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index c753c89627d..45ce20af628 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -34,7 +34,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) @@ -90,7 +89,6 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 4a18e639315..9c510bb3db0 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -256,7 +256,6 @@ async def test_options_flow(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -306,7 +305,6 @@ async def test_options_flow_invalid_auth(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -346,7 +344,6 @@ async def test_options_flow_cannot_connect(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index be822371030..2a7b5ed7084 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -61,7 +61,6 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, - system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 62a52b500f9..d583cad86c3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -943,7 +943,6 @@ async def test_restoring_client(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5c4a65e0a78..ad277f18a8d 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -793,7 +793,6 @@ async def test_restore_client_succeed(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) @@ -884,7 +883,6 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index f265b36dcb6..04d46620013 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -286,7 +286,6 @@ async def setup_ozw(hass, mock_openzwave): "Mock Title", {"usb_path": "mock-path", "network_key": "mock-key"}, "test", - system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 944f02d46c0..65a46f33cd8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -60,9 +60,7 @@ async def test_polling_only_updates_entities_it_should_poll(hass): async def test_polling_disabled_by_config_entry(hass): """Test the polling of only updated entities.""" entity_platform = MockEntityPlatform(hass) - entity_platform.config_entry = MockConfigEntry( - system_options={"disable_polling": True} - ) + entity_platform.config_entry = MockConfigEntry(pref_disable_polling=True) poll_ent = MockEntity(should_poll=True) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a124e1e6da1..fe445e32c96 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -513,12 +513,12 @@ async def test_disabled_by(registry): assert entry2.disabled_by is None -async def test_disabled_by_system_options(registry): - """Test system options setting disabled_by.""" +async def test_disabled_by_config_entry_pref(registry): + """Test config entry preference setting disabled_by.""" mock_config = MockConfigEntry( domain="light", entry_id="mock-id-1", - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index a0ce751aed8..7023798f2b4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -376,7 +376,7 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): async def test_not_schedule_refresh_if_system_option_disable_polling(hass): """Test we do not schedule a refresh if disable polling in config entry.""" - entry = MockConfigEntry(system_options={"disable_polling": True}) + entry = MockConfigEntry(pref_disable_polling=True) config_entries.current_entry.set(entry) crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 18791b1eb2d..1fca4b061cc 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -575,6 +575,13 @@ async def test_saving_and_loading(hass): ) assert len(hass.config_entries.async_entries()) == 2 + entry_1 = hass.config_entries.async_entries()[0] + + hass.config_entries.async_update_entry( + entry_1, + pref_disable_polling=True, + pref_disable_new_entities=True, + ) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) @@ -597,6 +604,8 @@ async def test_saving_and_loading(hass): assert orig.data == loaded.data assert orig.source == loaded.source assert orig.unique_id == loaded.unique_id + assert orig.pref_disable_new_entities == loaded.pref_disable_new_entities + assert orig.pref_disable_polling == loaded.pref_disable_polling async def test_forward_entry_sets_up_component(hass): @@ -814,14 +823,19 @@ async def test_updating_entry_system_options(manager): domain="test", data={"first": True}, state=config_entries.ConfigEntryState.SETUP_ERROR, - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry.add_to_manager(manager) - assert entry.system_options.disable_new_entities + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False - entry.system_options.update(disable_new_entities=False) - assert not entry.system_options.disable_new_entities + manager.async_update_entry( + entry, pref_disable_new_entities=False, pref_disable_polling=True + ) + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry_options_and_trigger_listener(hass, manager): @@ -2558,48 +2572,18 @@ async def test_updating_entry_with_and_without_changes(manager): entry.add_to_manager(manager) assert manager.async_update_entry(entry) is False - assert manager.async_update_entry(entry, data={"second": True}) is True - assert manager.async_update_entry(entry, data={"second": True}) is False - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is True - ) - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is False - ) - assert manager.async_update_entry(entry, options={"second": True}) is True - assert manager.async_update_entry(entry, options={"second": True}) is False - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is True - ) - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is False - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is True - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is False - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is True - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is False - ) - assert manager.async_update_entry(entry, title="thetitle") is False - assert manager.async_update_entry(entry, title="newtitle") is True - assert manager.async_update_entry(entry, unique_id="abc123") is False - assert manager.async_update_entry(entry, unique_id="abc1234") is True + + for change in ( + {"data": {"second": True, "third": 456}}, + {"data": {"second": True}}, + {"options": {"hello": True}}, + {"pref_disable_new_entities": True}, + {"pref_disable_polling": True}, + {"title": "sometitle"}, + {"unique_id": "abcd1234"}, + ): + assert manager.async_update_entry(entry, **change) is True + assert manager.async_update_entry(entry, **change) is False async def test_entry_reload_calls_on_unload_listeners(hass, manager): @@ -2864,3 +2848,35 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == reason + + +async def test_loading_old_data(hass, hass_storage): + """Test automatically migrating old data.""" + hass_storage[config_entries.STORAGE_KEY] = { + "version": 1, + "data": { + "entries": [ + { + "version": 5, + "domain": "my_domain", + "entry_id": "mock-id", + "data": {"my": "data"}, + "source": "user", + "title": "Mock title", + "system_options": {"disable_new_entities": True}, + } + ] + }, + } + manager = config_entries.ConfigEntries(hass, {}) + await manager.async_initialize() + + entries = manager.async_entries() + assert len(entries) == 1 + entry = entries[0] + assert entry.version == 5 + assert entry.domain == "my_domain" + assert entry.entry_id == "mock-id" + assert entry.title == "Mock title" + assert entry.data == {"my": "data"} + assert entry.pref_disable_new_entities is True From bee89a12ec19a5ec6c281f01fb3f3b6ea2d37468 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Jun 2021 22:35:13 +0200 Subject: [PATCH 136/750] Update frontend to 20210601.1 (#51354) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 0e0685cd772..42f29f36976 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210601.0" + "home-assistant-frontend==20210601.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 50d46da672d..c83f4625894 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 httpx==0.18.0 ifaddr==0.1.7 jinja2>=3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 3a027685f62..ba84ed28def 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 956dc0b37e7..05dcb71313c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5d33cd05a8f262ee4853e8c00c534ec63c6921b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Jun 2021 22:50:32 +0200 Subject: [PATCH 137/750] SolarEdge: Move coordinators out of sensor platform (#51348) --- .coveragerc | 1 + .../components/solaredge/coordinator.py | 280 +++++++++++++++++ homeassistant/components/solaredge/sensor.py | 290 +----------------- 3 files changed, 291 insertions(+), 280 deletions(-) create mode 100644 homeassistant/components/solaredge/coordinator.py diff --git a/.coveragerc b/.coveragerc index 9357f6d9972..5eab1aab00e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -944,6 +944,7 @@ omit = homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py homeassistant/components/solaredge/__init__.py + homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/* diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py new file mode 100644 index 00000000000..b2fe27db808 --- /dev/null +++ b/homeassistant/components/solaredge/coordinator.py @@ -0,0 +1,280 @@ +"""Provides the data update coordinators for SolarEdge.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import date, datetime, timedelta + +from solaredge import Solaredge +from stringcase import snakecase + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + DETAILS_UPDATE_DELAY, + ENERGY_DETAILS_DELAY, + INVENTORY_UPDATE_DELAY, + LOGGER, + OVERVIEW_UPDATE_DELAY, + POWER_FLOW_UPDATE_DELAY, +) + + +class SolarEdgeDataService: + """Get and update the latest data.""" + + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + """Initialize the data object.""" + self.api = api + self.site_id = site_id + + self.data = {} + self.attributes = {} + + self.hass = hass + self.coordinator = None + + @callback + def async_setup(self) -> None: + """Coordinator creation.""" + self.coordinator = DataUpdateCoordinator( + self.hass, + LOGGER, + name=str(self), + update_method=self.async_update_data, + update_interval=self.update_interval, + ) + + @property + @abstractmethod + def update_interval(self) -> timedelta: + """Update interval.""" + + @abstractmethod + def update(self) -> None: + """Update data in executor.""" + + async def async_update_data(self) -> None: + """Update data.""" + await self.hass.async_add_executor_job(self.update) + + +class SolarEdgeOverviewDataService(SolarEdgeDataService): + """Get and update the latest overview data.""" + + @property + def update_interval(self) -> timedelta: + """Update interval.""" + return OVERVIEW_UPDATE_DELAY + + def update(self) -> None: + """Update the data from the SolarEdge Monitoring API.""" + try: + data = self.api.get_overview(self.site_id) + overview = data["overview"] + except KeyError as ex: + raise UpdateFailed("Missing overview data, skipping update") from ex + + self.data = {} + + for key, value in overview.items(): + if key in ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"]: + data = value["energy"] + elif key in ["currentPower"]: + data = value["power"] + else: + data = value + self.data[key] = data + + LOGGER.debug("Updated SolarEdge overview: %s", self.data) + + +class SolarEdgeDetailsDataService(SolarEdgeDataService): + """Get and update the latest details data.""" + + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + """Initialize the details data service.""" + super().__init__(hass, api, site_id) + + self.data = None + + @property + def update_interval(self) -> timedelta: + """Update interval.""" + return DETAILS_UPDATE_DELAY + + def update(self) -> None: + """Update the data from the SolarEdge Monitoring API.""" + + try: + data = self.api.get_details(self.site_id) + details = data["details"] + except KeyError as ex: + raise UpdateFailed("Missing details data, skipping update") from ex + + self.data = None + self.attributes = {} + + for key, value in details.items(): + key = snakecase(key) + + if key in ["primary_module"]: + for module_key, module_value in value.items(): + self.attributes[snakecase(module_key)] = module_value + elif key in [ + "peak_power", + "type", + "name", + "last_update_time", + "installation_date", + ]: + self.attributes[key] = value + elif key == "status": + self.data = value + + LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) + + +class SolarEdgeInventoryDataService(SolarEdgeDataService): + """Get and update the latest inventory data.""" + + @property + def update_interval(self) -> timedelta: + """Update interval.""" + return INVENTORY_UPDATE_DELAY + + def update(self) -> None: + """Update the data from the SolarEdge Monitoring API.""" + try: + data = self.api.get_inventory(self.site_id) + inventory = data["Inventory"] + except KeyError as ex: + raise UpdateFailed("Missing inventory data, skipping update") from ex + + self.data = {} + self.attributes = {} + + for key, value in inventory.items(): + self.data[key] = len(value) + self.attributes[key] = {key: value} + + LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes) + + +class SolarEdgeEnergyDetailsService(SolarEdgeDataService): + """Get and update the latest power flow data.""" + + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + """Initialize the power flow data service.""" + super().__init__(hass, api, site_id) + + self.unit = None + + @property + def update_interval(self) -> timedelta: + """Update interval.""" + return ENERGY_DETAILS_DELAY + + def update(self) -> None: + """Update the data from the SolarEdge Monitoring API.""" + try: + now = datetime.now() + today = date.today() + midnight = datetime.combine(today, datetime.min.time()) + data = self.api.get_energy_details( + self.site_id, + midnight, + now.strftime("%Y-%m-%d %H:%M:%S"), + meters=None, + time_unit="DAY", + ) + energy_details = data["energyDetails"] + except KeyError as ex: + raise UpdateFailed("Missing power flow data, skipping update") from ex + + if "meters" not in energy_details: + LOGGER.debug( + "Missing meters in energy details data. Assuming site does not have any" + ) + return + + self.data = {} + self.attributes = {} + self.unit = energy_details["unit"] + + for meter in energy_details["meters"]: + if "type" not in meter or "values" not in meter: + continue + if meter["type"] not in [ + "Production", + "SelfConsumption", + "FeedIn", + "Purchased", + "Consumption", + ]: + continue + if len(meter["values"][0]) == 2: + self.data[meter["type"]] = meter["values"][0]["value"] + self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} + + LOGGER.debug( + "Updated SolarEdge energy details: %s, %s", self.data, self.attributes + ) + + +class SolarEdgePowerFlowDataService(SolarEdgeDataService): + """Get and update the latest power flow data.""" + + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + """Initialize the power flow data service.""" + super().__init__(hass, api, site_id) + + self.unit = None + + @property + def update_interval(self) -> timedelta: + """Update interval.""" + return POWER_FLOW_UPDATE_DELAY + + def update(self) -> None: + """Update the data from the SolarEdge Monitoring API.""" + try: + data = self.api.get_current_power_flow(self.site_id) + power_flow = data["siteCurrentPowerFlow"] + except KeyError as ex: + raise UpdateFailed("Missing power flow data, skipping update") from ex + + power_from = [] + power_to = [] + + if "connections" not in power_flow: + LOGGER.debug( + "Missing connections in power flow data. Assuming site does not have any" + ) + return + + for connection in power_flow["connections"]: + power_from.append(connection["from"].lower()) + power_to.append(connection["to"].lower()) + + self.data = {} + self.attributes = {} + self.unit = power_flow["unit"] + + for key, value in power_flow.items(): + if key in ["LOAD", "PV", "GRID", "STORAGE"]: + self.data[key] = value["currentPower"] + self.attributes[key] = {"status": value["status"]} + + if key in ["GRID"]: + export = key.lower() in power_to + self.data[key] *= -1 if export else 1 + self.attributes[key]["flow"] = "export" if export else "import" + + if key in ["STORAGE"]: + charge = key.lower() in power_to + self.data[key] *= -1 if charge else 1 + self.attributes[key]["flow"] = "charge" if charge else "discharge" + self.attributes[key]["soc"] = value["chargeLevel"] + + LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 75bb25f722d..5cd644f5006 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,35 +1,25 @@ """Support for SolarEdge Monitoring API.""" from __future__ import annotations -from abc import abstractmethod -from datetime import date, datetime, timedelta from typing import Any from solaredge import Solaredge -from stringcase import snakecase from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - CONF_SITE_ID, - DATA_API_CLIENT, - DETAILS_UPDATE_DELAY, - DOMAIN, - ENERGY_DETAILS_DELAY, - INVENTORY_UPDATE_DELAY, - LOGGER, - OVERVIEW_UPDATE_DELAY, - POWER_FLOW_UPDATE_DELAY, - SENSOR_TYPES, +from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, SENSOR_TYPES +from .coordinator import ( + SolarEdgeDataService, + SolarEdgeDetailsDataService, + SolarEdgeEnergyDetailsService, + SolarEdgeInventoryDataService, + SolarEdgeOverviewDataService, + SolarEdgePowerFlowDataService, ) @@ -249,263 +239,3 @@ class SolarEdgeStorageLevelSensor(SolarEdgeSensor): if attr and "soc" in attr: return attr["soc"] return None - - -class SolarEdgeDataService: - """Get and update the latest data.""" - - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the data object.""" - self.api = api - self.site_id = site_id - - self.data = {} - self.attributes = {} - - self.hass = hass - self.coordinator = None - - @callback - def async_setup(self) -> None: - """Coordinator creation.""" - self.coordinator = DataUpdateCoordinator( - self.hass, - LOGGER, - name=str(self), - update_method=self.async_update_data, - update_interval=self.update_interval, - ) - - @property - @abstractmethod - def update_interval(self) -> timedelta: - """Update interval.""" - - @abstractmethod - def update(self) -> None: - """Update data in executor.""" - - async def async_update_data(self) -> None: - """Update data.""" - await self.hass.async_add_executor_job(self.update) - - -class SolarEdgeOverviewDataService(SolarEdgeDataService): - """Get and update the latest overview data.""" - - @property - def update_interval(self) -> timedelta: - """Update interval.""" - return OVERVIEW_UPDATE_DELAY - - def update(self) -> None: - """Update the data from the SolarEdge Monitoring API.""" - try: - data = self.api.get_overview(self.site_id) - overview = data["overview"] - except KeyError as ex: - raise UpdateFailed("Missing overview data, skipping update") from ex - - self.data = {} - - for key, value in overview.items(): - if key in ["lifeTimeData", "lastYearData", "lastMonthData", "lastDayData"]: - data = value["energy"] - elif key in ["currentPower"]: - data = value["power"] - else: - data = value - self.data[key] = data - - LOGGER.debug("Updated SolarEdge overview: %s", self.data) - - -class SolarEdgeDetailsDataService(SolarEdgeDataService): - """Get and update the latest details data.""" - - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the details data service.""" - super().__init__(hass, api, site_id) - - self.data = None - - @property - def update_interval(self) -> timedelta: - """Update interval.""" - return DETAILS_UPDATE_DELAY - - def update(self) -> None: - """Update the data from the SolarEdge Monitoring API.""" - - try: - data = self.api.get_details(self.site_id) - details = data["details"] - except KeyError as ex: - raise UpdateFailed("Missing details data, skipping update") from ex - - self.data = None - self.attributes = {} - - for key, value in details.items(): - key = snakecase(key) - - if key in ["primary_module"]: - for module_key, module_value in value.items(): - self.attributes[snakecase(module_key)] = module_value - elif key in [ - "peak_power", - "type", - "name", - "last_update_time", - "installation_date", - ]: - self.attributes[key] = value - elif key == "status": - self.data = value - - LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) - - -class SolarEdgeInventoryDataService(SolarEdgeDataService): - """Get and update the latest inventory data.""" - - @property - def update_interval(self) -> timedelta: - """Update interval.""" - return INVENTORY_UPDATE_DELAY - - def update(self) -> None: - """Update the data from the SolarEdge Monitoring API.""" - try: - data = self.api.get_inventory(self.site_id) - inventory = data["Inventory"] - except KeyError as ex: - raise UpdateFailed("Missing inventory data, skipping update") from ex - - self.data = {} - self.attributes = {} - - for key, value in inventory.items(): - self.data[key] = len(value) - self.attributes[key] = {key: value} - - LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes) - - -class SolarEdgeEnergyDetailsService(SolarEdgeDataService): - """Get and update the latest power flow data.""" - - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the power flow data service.""" - super().__init__(hass, api, site_id) - - self.unit = None - - @property - def update_interval(self) -> timedelta: - """Update interval.""" - return ENERGY_DETAILS_DELAY - - def update(self) -> None: - """Update the data from the SolarEdge Monitoring API.""" - try: - now = datetime.now() - today = date.today() - midnight = datetime.combine(today, datetime.min.time()) - data = self.api.get_energy_details( - self.site_id, - midnight, - now.strftime("%Y-%m-%d %H:%M:%S"), - meters=None, - time_unit="DAY", - ) - energy_details = data["energyDetails"] - except KeyError as ex: - raise UpdateFailed("Missing power flow data, skipping update") from ex - - if "meters" not in energy_details: - LOGGER.debug( - "Missing meters in energy details data. Assuming site does not have any" - ) - return - - self.data = {} - self.attributes = {} - self.unit = energy_details["unit"] - - for meter in energy_details["meters"]: - if "type" not in meter or "values" not in meter: - continue - if meter["type"] not in [ - "Production", - "SelfConsumption", - "FeedIn", - "Purchased", - "Consumption", - ]: - continue - if len(meter["values"][0]) == 2: - self.data[meter["type"]] = meter["values"][0]["value"] - self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]} - - LOGGER.debug( - "Updated SolarEdge energy details: %s, %s", self.data, self.attributes - ) - - -class SolarEdgePowerFlowDataService(SolarEdgeDataService): - """Get and update the latest power flow data.""" - - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the power flow data service.""" - super().__init__(hass, api, site_id) - - self.unit = None - - @property - def update_interval(self) -> timedelta: - """Update interval.""" - return POWER_FLOW_UPDATE_DELAY - - def update(self) -> None: - """Update the data from the SolarEdge Monitoring API.""" - try: - data = self.api.get_current_power_flow(self.site_id) - power_flow = data["siteCurrentPowerFlow"] - except KeyError as ex: - raise UpdateFailed("Missing power flow data, skipping update") from ex - - power_from = [] - power_to = [] - - if "connections" not in power_flow: - LOGGER.debug( - "Missing connections in power flow data. Assuming site does not have any" - ) - return - - for connection in power_flow["connections"]: - power_from.append(connection["from"].lower()) - power_to.append(connection["to"].lower()) - - self.data = {} - self.attributes = {} - self.unit = power_flow["unit"] - - for key, value in power_flow.items(): - if key in ["LOAD", "PV", "GRID", "STORAGE"]: - self.data[key] = value["currentPower"] - self.attributes[key] = {"status": value["status"]} - - if key in ["GRID"]: - export = key.lower() in power_to - self.data[key] *= -1 if export else 1 - self.attributes[key]["flow"] = "export" if export else "import" - - if key in ["STORAGE"]: - charge = key.lower() in power_to - self.data[key] *= -1 if charge else 1 - self.attributes[key]["flow"] = "charge" if charge else "discharge" - self.attributes[key]["soc"] = value["chargeLevel"] - - LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes) From 783e545a67ee6088136023256ec0b5fbad07f17d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 1 Jun 2021 22:55:22 +0200 Subject: [PATCH 138/750] Bump hangups to 0.4.14 (#51355) --- homeassistant/components/hangouts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 69cfa515c02..98531fca11a 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -3,7 +3,7 @@ "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": ["hangups==0.4.11"], + "requirements": ["hangups==0.4.14"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index ba84ed28def..651be9eecfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -729,7 +729,7 @@ ha-philipsjs==2.7.4 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.11 +hangups==0.4.14 # homeassistant.components.cloud hass-nabucasa==0.43.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05dcb71313c..d1123404465 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ ha-philipsjs==2.7.4 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.11 +hangups==0.4.14 # homeassistant.components.cloud hass-nabucasa==0.43.0 From 10dccc673437db223b94eff8a7b6f1990d100c85 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 2 Jun 2021 00:00:44 +0200 Subject: [PATCH 139/750] Move pymodbus test fixtures to test_init (#51244) --- tests/components/modbus/conftest.py | 1 + tests/components/modbus/test_init.py | 297 +++++++++++++++++++-------- 2 files changed, 211 insertions(+), 87 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d066e33437c..7f67a0653fb 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -57,6 +57,7 @@ async def mock_modbus(hass, mock_pymodbus): yield mock_pymodbus +# dataclass class ReadResult: """Storage class for register read results.""" diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8e45ee06976..0e9bf43eec2 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -5,9 +5,12 @@ This file is responsible for testing: - Functionality of class ModbusHub - Coverage 100%: __init__.py - base_platform.py const.py modbus.py + validators.py + baseplatform.py (only BasePlatform) + +It uses binary_sensors/sensors to do black box testing of the read calls. """ from datetime import timedelta import logging @@ -35,19 +38,31 @@ from homeassistant.components.modbus.const import ( CALL_TYPE_WRITE_REGISTERS, CONF_BAUDRATE, CONF_BYTESIZE, + CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_PARITY, + CONF_REVERSE_ORDER, CONF_STOPBITS, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_WORD, + DATA_TYPE_CUSTOM, + DATA_TYPE_INT, + DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) -from homeassistant.components.modbus.validators import number_validator +from homeassistant.components.modbus.validators import ( + number_validator, + sensor_schema_validator, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_ADDRESS, CONF_BINARY_SENSORS, + CONF_COUNT, CONF_DELAY, CONF_HOST, CONF_METHOD, @@ -55,6 +70,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, + CONF_STRUCTURE, CONF_TIMEOUT, CONF_TYPE, STATE_ON, @@ -63,13 +79,27 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import TEST_MODBUS_NAME, ReadResult +from .conftest import ReadResult from tests.common import async_fire_time_changed TEST_SENSOR_NAME = "testSensor" TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" TEST_HOST = "modbusTestHost" +TEST_MODBUS_NAME = "modbusTest" + + +@pytest.fixture +async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus): + """Load integration modbus using mocked pymodbus.""" + caplog.clear() + caplog.set_level(logging.ERROR) + config = {DOMAIN: do_config} + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert DOMAIN in hass.config.components + assert caplog.text == "" + yield mock_pymodbus async def test_number_validator(): @@ -94,6 +124,84 @@ async def test_number_validator(): pytest.fail("Number_validator not throwing exception") +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_STRING, + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_REVERSE_ORDER: True, + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_REVERSE_ORDER: False, + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_BYTE, + }, + ], +) +async def test_ok_sensor_schema_validator(do_config): + """Test struct validator.""" + try: + sensor_schema_validator(do_config) + except vol.Invalid: + pytest.fail("Sensor_schema_validator unexpected exception") + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 8, + CONF_DATA_TYPE: DATA_TYPE_INT, + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 8, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 8, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: "no good", + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 20, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">f", + }, + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SWAP: CONF_SWAP_WORD, + }, + ], +) +async def test_exception_sensor_schema_validator(do_config): + """Test struct validator.""" + try: + sensor_schema_validator(do_config) + except vol.Invalid: + return + pytest.fail("Sensor_schema_validator missing exception") + + @pytest.mark.parametrize( "do_config", [ @@ -187,16 +295,23 @@ async def test_number_validator(): CONF_NAME: TEST_MODBUS_NAME + "3", }, ], + { + # Special test for scan_interval validator with scan_interval: 0 + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_SENSORS: [ + { + CONF_NAME: TEST_SENSOR_NAME, + CONF_ADDRESS: 117, + CONF_SCAN_INTERVAL: 0, + } + ], + }, ], ) -async def test_config_modbus(hass, caplog, do_config, mock_pymodbus): +async def test_config_modbus(hass, caplog, mock_modbus_with_pymodbus): """Run configuration test for modbus.""" - config = {DOMAIN: do_config} - caplog.set_level(logging.ERROR) - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - assert DOMAIN in hass.config.components - assert len(caplog.records) == 0 VALUE = "value" @@ -205,6 +320,17 @@ DATA = "data" SERVICE = "service" +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + }, + ], +) @pytest.mark.parametrize( "do_write", [ @@ -234,14 +360,25 @@ SERVICE = "service" }, ], ) -async def test_pb_service_write(hass, do_write, caplog, mock_modbus): +@pytest.mark.parametrize( + "do_return", + [ + {VALUE: ReadResult([0x0001]), DATA: ""}, + {VALUE: ExceptionResponse(0x06), DATA: "Pymodbus:"}, + {VALUE: IllegalFunctionRequest(0x06), DATA: "Pymodbus:"}, + {VALUE: ModbusException("fail write_"), DATA: "Pymodbus:"}, + ], +) +async def test_pb_service_write( + hass, do_write, do_return, caplog, mock_modbus_with_pymodbus +): """Run test for service write_register.""" func_name = { - CALL_TYPE_WRITE_COIL: mock_modbus.write_coil, - CALL_TYPE_WRITE_COILS: mock_modbus.write_coils, - CALL_TYPE_WRITE_REGISTER: mock_modbus.write_register, - CALL_TYPE_WRITE_REGISTERS: mock_modbus.write_registers, + CALL_TYPE_WRITE_COIL: mock_modbus_with_pymodbus.write_coil, + CALL_TYPE_WRITE_COILS: mock_modbus_with_pymodbus.write_coils, + CALL_TYPE_WRITE_REGISTER: mock_modbus_with_pymodbus.write_register, + CALL_TYPE_WRITE_REGISTERS: mock_modbus_with_pymodbus.write_registers, } data = { @@ -250,28 +387,42 @@ async def test_pb_service_write(hass, do_write, caplog, mock_modbus): ATTR_ADDRESS: 16, do_write[DATA]: do_write[VALUE], } + mock_modbus_with_pymodbus.reset_mock() + caplog.clear() + caplog.set_level(logging.DEBUG) + func_name[do_write[FUNC]].return_value = do_return[VALUE] await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) assert func_name[do_write[FUNC]].called assert func_name[do_write[FUNC]].call_args[0] == ( data[ATTR_ADDRESS], data[do_write[DATA]], ) - mock_modbus.reset_mock() - - for return_value in [ - ExceptionResponse(0x06), - IllegalFunctionRequest(0x06), - ModbusException("fail write_"), - ]: - caplog.set_level(logging.DEBUG) - func_name[do_write[FUNC]].return_value = return_value - await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) - assert func_name[do_write[FUNC]].called + if do_return[DATA]: assert caplog.messages[-1].startswith("Pymodbus:") - mock_modbus.reset_mock() -async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_pymodbus): +@pytest.fixture +async def mock_modbus_read_pymodbus( + hass, + do_group, + do_type, + do_scan_interval, + do_return, + do_exception, + caplog, + mock_pymodbus, +): + """Load integration modbus using mocked pymodbus.""" + caplog.clear() + caplog.set_level(logging.ERROR) + mock_pymodbus.read_coils.side_effect = do_exception + mock_pymodbus.read_discrete_inputs.side_effect = do_exception + mock_pymodbus.read_input_registers.side_effect = do_exception + mock_pymodbus.read_holding_registers.side_effect = do_exception + mock_pymodbus.read_coils.return_value = do_return + mock_pymodbus.read_discrete_inputs.return_value = do_return + mock_pymodbus.read_input_registers.return_value = do_return + mock_pymodbus.read_holding_registers.return_value = do_return config = { DOMAIN: [ { @@ -284,91 +435,63 @@ async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_py CONF_INPUT_TYPE: do_type, CONF_NAME: TEST_SENSOR_NAME, CONF_ADDRESS: 51, - CONF_SCAN_INTERVAL: 1, + CONF_SCAN_INTERVAL: do_scan_interval, } ], } - ] + ], } - mock_pymodbus.read_coils.side_effect = do_exception - mock_pymodbus.read_discrete_inputs.side_effect = do_exception - mock_pymodbus.read_input_registers.side_effect = do_exception - mock_pymodbus.read_holding_registers.side_effect = do_exception - mock_pymodbus.read_coils.return_value = do_return - mock_pymodbus.read_discrete_inputs.return_value = do_return - mock_pymodbus.read_input_registers.return_value = do_return - mock_pymodbus.read_holding_registers.return_value = do_return now = dt_util.utcnow() with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() + assert DOMAIN in hass.config.components + assert caplog.text == "" now = now + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60) with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() + yield mock_pymodbus @pytest.mark.parametrize( - "do_return,do_exception,do_expect", + "do_domain, do_group,do_type,do_scan_interval", [ - [ReadResult([7]), None, "7"], - [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE], - [ExceptionResponse(0x99), None, STATE_UNAVAILABLE], - [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE], + [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 1], + [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 1], + [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 1], + [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_COIL, 1], ], ) @pytest.mark.parametrize( - "do_type", - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT], + "do_return,do_exception,do_expect_state,do_expect_value", + [ + [ReadResult([1]), None, STATE_ON, "1"], + [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE], + [ExceptionResponse(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE], + [ + ReadResult([1]), + ModbusException("fail read_"), + STATE_UNAVAILABLE, + STATE_UNAVAILABLE, + ], + ], ) -async def test_pb_read_value( - hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus +async def test_pb_read( + hass, do_domain, do_expect_state, do_expect_value, caplog, mock_modbus_read_pymodbus ): """Run test for different read.""" - # the purpose of this test is to test the special - # return values from pymodbus: - # ExceptionResponse, IllegalResponse - # and exceptions. - # We "hijiack" binary_sensor and sensor in order - # to make a proper blackbox test. - await _read_helper( - hass, CONF_SENSORS, do_type, do_return, do_exception, mock_pymodbus - ) - # Check state - entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + entity_id = f"{do_domain}.{TEST_SENSOR_NAME}" + state = hass.states.get(entity_id).state assert hass.states.get(entity_id).state - -@pytest.mark.parametrize( - "do_return,do_exception,do_expect", - [ - [ReadResult([0x01]), None, STATE_ON], - [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE], - [ExceptionResponse(0x99), None, STATE_UNAVAILABLE], - [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE], - ], -) -@pytest.mark.parametrize("do_type", [CALL_TYPE_DISCRETE, CALL_TYPE_COIL]) -async def test_pb_read_state( - hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus -): - """Run test for different read.""" - - # the purpose of this test is to test the special - # return values from pymodbus: - # ExceptionResponse, IllegalResponse - # and exceptions. - # We "hijiack" binary_sensor and sensor in order - # to make a proper blackbox test. - await _read_helper( - hass, CONF_BINARY_SENSORS, do_type, do_return, do_exception, mock_pymodbus - ) - - # Check state - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" - state = hass.states.get(entity_id).state + # this if is needed to avoid explode the + if do_domain == SENSOR_DOMAIN: + do_expect = do_expect_value + else: + do_expect = do_expect_state assert state == do_expect From 101864aab8d4777a9ca2a442a7ceae2d17058cc8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 Jun 2021 21:35:12 -0600 Subject: [PATCH 140/750] Bump pyiqvia to 1.0.0 (#51357) --- homeassistant/components/iqvia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 779e62de4fb..75249ded6a1 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.20.3", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.3", "pyiqvia==1.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 651be9eecfc..717384873af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1481,7 +1481,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1123404465..0811e20b707 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -822,7 +822,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.isy994 pyisy==3.0.0 From d51fc5814ae5dc5fd8096691ae258a6c27ed4980 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Jun 2021 08:53:55 +0200 Subject: [PATCH 141/750] Define ToggleEntity entity attributes as class variables (#51231) * Define ToggleEntity entity attributes as class variables * Fix upcloud overriding state property * Implement available state for upcloud, to compensate removed state --- homeassistant/components/upcloud/__init__.py | 20 +++++++++++--------- homeassistant/helpers/entity.py | 8 ++++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 925d41a3252..636fa7a2b8a 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -273,18 +273,20 @@ class UpCloudServerEntity(CoordinatorEntity): """Return the icon of this server.""" return "mdi:server" if self.is_on else "mdi:server-off" - @property - def state(self) -> str | None: - """Return state of the server.""" - try: - return STATE_MAP.get(self._server.state, self._server.state) - except AttributeError: - return None - @property def is_on(self) -> bool: """Return true if the server is on.""" - return self.state == STATE_ON + try: + return STATE_MAP.get(self._server.state, self._server.state) == STATE_ON + except AttributeError: + return False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and STATE_MAP.get( + self._server.state, self._server.state + ) in [STATE_ON, STATE_OFF] @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 724280b19c9..4afab38fabf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -10,7 +10,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, TypedDict +from typing import Any, TypedDict, final from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -766,7 +766,11 @@ class Entity(ABC): class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" + _attr_is_on: bool + _attr_state: None = None + @property + @final def state(self) -> str | None: """Return the state.""" return STATE_ON if self.is_on else STATE_OFF @@ -774,7 +778,7 @@ class ToggleEntity(Entity): @property def is_on(self) -> bool: """Return True if entity is on.""" - raise NotImplementedError() + return self._attr_is_on def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" From 1d9d9021deed31337910a4a8d80feb665d33d0b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Jun 2021 10:00:24 +0200 Subject: [PATCH 142/750] Do not attempt to unload non loaded config entries (#51356) --- homeassistant/config_entries.py | 3 +++ tests/components/smartthings/test_binary_sensor.py | 2 ++ tests/components/smartthings/test_cover.py | 2 ++ tests/components/smartthings/test_fan.py | 2 ++ tests/components/smartthings/test_light.py | 2 ++ tests/components/smartthings/test_lock.py | 2 ++ tests/components/smartthings/test_scene.py | 2 ++ tests/components/smartthings/test_sensor.py | 2 ++ tests/components/smartthings/test_switch.py | 2 ++ 9 files changed, 19 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eeaf0149cc2..49892937217 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -392,6 +392,9 @@ class ConfigEntry: self.reason = None return True + if self.state == ConfigEntryState.NOT_LOADED: + return True + if integration is None: try: integration = await loader.async_get_integration(hass, self.domain) diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 7e8ab7d2c9b..f3d548c1e39 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -91,6 +92,7 @@ async def test_unload_config_entry(hass, device_factory): "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} ) config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 44c2b2f9285..aad7a4b037e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,6 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -191,6 +192,7 @@ async def test_unload_config_entry(hass, device_factory): "Garage", [Capability.garage_door_control], {Attribute.door: "open"} ) config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6cdfa5b8917..2a66fc646c7 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -186,6 +187,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: "off", Attribute.fan_speed: 0}, ) config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index c9dbb094161..81062adf934 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -306,6 +307,7 @@ async def test_unload_config_entry(hass, device_factory): }, ) config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 1168108656e..86c8d534a71 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -103,6 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "locked"}) config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 647389eeb42..288fae046f5 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,6 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -44,6 +45,7 @@ async def test_unload_config_entry(hass, scene): """Test the scene is removed when the config entry is unloaded.""" # Arrange config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 0f148b8931f..4af88e27fe4 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -9,6 +9,7 @@ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -116,6 +117,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 21d508bcbc2..7c202fad12e 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -95,6 +96,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Switch 1", [Capability.switch], {Attribute.switch: "on"}) config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert From 3de29a7606b1e15fc068e8a66cefdea147366063 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 2 Jun 2021 13:59:35 +0200 Subject: [PATCH 143/750] Add binary_sensor tests for devolo Home Control (#49843) Co-authored-by: Markus Bong --- .coveragerc | 2 - tests/components/devolo_home_control/mocks.py | 91 ++++++++++++++ .../devolo_home_control/test_binary_sensor.py | 112 ++++++++++++++++++ 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 tests/components/devolo_home_control/mocks.py create mode 100644 tests/components/devolo_home_control/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 5eab1aab00e..f788c917e82 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,11 +182,9 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/binary_sensor.py homeassistant/components/devolo_home_control/climate.py homeassistant/components/devolo_home_control/const.py homeassistant/components/devolo_home_control/cover.py - homeassistant/components/devolo_home_control/devolo_device.py homeassistant/components/devolo_home_control/devolo_multi_level_switch.py homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py new file mode 100644 index 00000000000..d2ba69d9440 --- /dev/null +++ b/tests/components/devolo_home_control/mocks.py @@ -0,0 +1,91 @@ +"""Mocks for tests.""" + +from unittest.mock import MagicMock + +from devolo_home_control_api.publisher.publisher import Publisher + + +class BinarySensorPropertyMock: + """devolo Home Control binary sensor mock.""" + + element_uid = "Test" + key_count = 1 + sensor_type = "door" + sub_type = "" + state = False + + +class SettingsMock: + """devolo Home Control settings mock.""" + + name = "Test" + zone = "Test" + + +class DeviceMock: + """devolo Home Control device mock.""" + + available = True + brand = "devolo" + name = "Test Device" + uid = "Test" + settings_property = {"general_device_settings": SettingsMock()} + + def is_online(self): + """Mock online state of the device.""" + return DeviceMock.available + + +class BinarySensorMock(DeviceMock): + """devolo Home Control binary sensor device mock.""" + + binary_sensor_property = {"Test": BinarySensorPropertyMock()} + + +class RemoteControlMock(DeviceMock): + """devolo Home Control remote control device mock.""" + + remote_control_property = {"Test": BinarySensorPropertyMock()} + + +class DisabledBinarySensorMock(DeviceMock): + """devolo Home Control disabled binary sensor device mock.""" + + binary_sensor_property = {"devolo.WarningBinaryFI:Test": BinarySensorPropertyMock()} + + +class HomeControlMock: + """devolo Home Control gateway mock.""" + + binary_sensor_devices = [] + binary_switch_devices = [] + multi_level_sensor_devices = [] + multi_level_switch_devices = [] + devices = {} + publisher = MagicMock() + + def websocket_disconnect(self): + """Mock disconnect of the websocket.""" + pass + + +class HomeControlMockBinarySensor(HomeControlMock): + """devolo Home Control gateway mock with binary sensor device.""" + + binary_sensor_devices = [BinarySensorMock()] + devices = {"Test": BinarySensorMock()} + publisher = Publisher(devices.keys()) + publisher.unregister = MagicMock() + + +class HomeControlMockRemoteControl(HomeControlMock): + """devolo Home Control gateway mock with remote control device.""" + + devices = {"Test": RemoteControlMock()} + publisher = Publisher(devices.keys()) + + +class HomeControlMockDisabledBinarySensor(HomeControlMock): + """devolo Home Control gateway mock with disabled device.""" + + binary_sensor_devices = [DisabledBinarySensorMock()] diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py new file mode 100644 index 00000000000..022cd4a1578 --- /dev/null +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -0,0 +1,112 @@ +"""Tests for the devolo Home Control binary sensors.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import ( + DeviceMock, + HomeControlMock, + HomeControlMockBinarySensor, + HomeControlMockDisabledBinarySensor, + HomeControlMockRemoteControl, +) + + +@pytest.mark.usefixtures("mock_zeroconf") +async def test_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a binary sensor device.""" + entry = configure_integration(hass) + DeviceMock.available = True + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[HomeControlMockBinarySensor, HomeControlMock], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_OFF + + # Emulate websocket message: sensor turned on + HomeControlMockBinarySensor.publisher.dispatch("Test", ("Test", True)) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON + + # Emulate websocket message: device went offline + DeviceMock.available = False + HomeControlMockBinarySensor.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("mock_zeroconf") +async def test_remote_control(hass: HomeAssistant): + """Test setup and state change of a remote control device.""" + entry = configure_integration(hass) + DeviceMock.available = True + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[HomeControlMockRemoteControl, HomeControlMock], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_OFF + + # Emulate websocket message: button pressed + HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 1)) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_ON + + # Emulate websocket message: button released + HomeControlMockRemoteControl.publisher.dispatch("Test", ("Test", 0)) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_OFF + + # Emulate websocket message: device went offline + DeviceMock.available = False + HomeControlMockRemoteControl.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("mock_zeroconf") +async def test_disabled(hass: HomeAssistant): + """Test setup of a disabled device.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[HomeControlMockDisabledBinarySensor, HomeControlMock], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{DOMAIN}.devolo.WarningBinaryFI:Test") is None + + +@pytest.mark.usefixtures("mock_zeroconf") +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[HomeControlMockBinarySensor, HomeControlMock], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + HomeControlMockBinarySensor.publisher.unregister.assert_called_once() From 931ff70ebe516ac7b57dc017b91e3ca162cb894b Mon Sep 17 00:00:00 2001 From: gadgetmobile <57815233+gadgetmobile@users.noreply.github.com> Date: Wed, 2 Jun 2021 14:02:37 +0200 Subject: [PATCH 144/750] Fix BleBox wLightBoxS and gateBox support (#51367) Co-authored-by: bbx-jp <83213200+bbx-jp@users.noreply.github.com> --- CODEOWNERS | 2 +- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7f2dfc1454e..405c624c22d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -64,7 +64,7 @@ homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria -homeassistant/components/blebox/* @gadgetmobile +homeassistant/components/blebox/* @bbx-a @bbx-jp homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 00b4b61c507..39c0d37e2e3 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==1.3.2"], - "codeowners": ["@gadgetmobile"], + "requirements": ["blebox_uniapi==1.3.3"], + "codeowners": ["@bbx-a", "@bbx-jp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 717384873af..0235499d0d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ bimmer_connected==0.7.15 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0811e20b707..21733c61b71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ bellows==0.24.0 bimmer_connected==0.7.15 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 From 25c0739e224d8bf3feb89f19da1151267555bbc2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Jun 2021 14:32:25 +0200 Subject: [PATCH 145/750] Mark state final in BinarySensorEntity (#51234) --- homeassistant/components/binary_sensor/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index c7e1bac9952..d698a9c306e 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import final import voluptuous as vol @@ -150,12 +151,14 @@ class BinarySensorEntity(Entity): """Represent a binary sensor.""" _attr_is_on: bool | None = None + _attr_state: None = None @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._attr_is_on + @final @property def state(self) -> StateType: """Return the state of the binary sensor.""" From 31bd41582bee617cfcbc51f43d06bac895cd0371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Jun 2021 17:16:04 +0200 Subject: [PATCH 146/750] Fix Tibber timestamps parsing (#51368) --- homeassistant/components/tibber/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 70dfa54c70a..f2ff23dfe5d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -385,7 +385,7 @@ class TibberRtDataHandler: if live_measurement is None: return - timestamp = datetime.fromisoformat(live_measurement.pop("timestamp")) + timestamp = dt_util.parse_datetime(live_measurement.pop("timestamp")) new_entities = [] for sensor_type, state in live_measurement.items(): if state is None or sensor_type not in RT_SENSOR_MAP: From 68714c2067c8f91f83ed14f52008f0e2e5be0d36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Jun 2021 11:10:33 -0500 Subject: [PATCH 147/750] Update ping to use asyncio function in icmplib (#50808) --- homeassistant/components/ping/__init__.py | 23 +--------------- .../components/ping/binary_sensor.py | 24 +++++++---------- homeassistant/components/ping/const.py | 3 --- .../components/ping/device_tracker.py | 18 +++++-------- homeassistant/components/ping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ping/test_init.py | 26 ------------------- 8 files changed, 19 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index b9a9f6460db..70b90ccd886 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -5,10 +5,9 @@ import logging from icmplib import SocketPermissionError, ping as icmp_ping -from homeassistant.core import callback from homeassistant.helpers.reload import async_setup_reload_service -from .const import DEFAULT_START_ID, DOMAIN, MAX_PING_ID, PING_ID, PING_PRIVS, PLATFORMS +from .const import DOMAIN, PING_PRIVS, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -18,30 +17,10 @@ async def async_setup(hass, config): await async_setup_reload_service(hass, DOMAIN, PLATFORMS) hass.data[DOMAIN] = { PING_PRIVS: await hass.async_add_executor_job(_can_use_icmp_lib_with_privilege), - PING_ID: DEFAULT_START_ID, } return True -@callback -def async_get_next_ping_id(hass, count=1): - """Find the next id to use in the outbound ping. - - When using multiping, we increment the id - by the number of ids that multiping - will use. - - Must be called in async - """ - allocated_id = hass.data[DOMAIN][PING_ID] + 1 - if allocated_id > MAX_PING_ID: - allocated_id -= MAX_PING_ID - DEFAULT_START_ID - hass.data[DOMAIN][PING_ID] += count - if hass.data[DOMAIN][PING_ID] > MAX_PING_ID: - hass.data[DOMAIN][PING_ID] -= MAX_PING_ID - DEFAULT_START_ID - return allocated_id - - def _can_use_icmp_lib_with_privilege() -> None | bool: """Verify we can create a raw socket.""" try: diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 9ae891d598b..cf2d8f7ed7a 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -4,13 +4,12 @@ from __future__ import annotations import asyncio from contextlib import suppress from datetime import timedelta -from functools import partial import logging import re import sys from typing import Any -from icmplib import NameLookupError, ping as icmp_ping +from icmplib import NameLookupError, async_ping import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -22,7 +21,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from . import async_get_next_ping_id from .const import DOMAIN, ICMP_TIMEOUT, PING_PRIVS, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -141,10 +139,10 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): attributes = last_state.attributes self._ping.is_alive = True self._ping.data = { - "min": attributes[ATTR_ROUND_TRIP_TIME_AVG], + "min": attributes[ATTR_ROUND_TRIP_TIME_MIN], "max": attributes[ATTR_ROUND_TRIP_TIME_MAX], - "avg": attributes[ATTR_ROUND_TRIP_TIME_MDEV], - "mdev": attributes[ATTR_ROUND_TRIP_TIME_MIN], + "avg": attributes[ATTR_ROUND_TRIP_TIME_AVG], + "mdev": attributes[ATTR_ROUND_TRIP_TIME_MDEV], } @@ -172,15 +170,11 @@ class PingDataICMPLib(PingData): """Retrieve the latest details from the host.""" _LOGGER.debug("ping address: %s", self._ip_address) try: - data = await self.hass.async_add_executor_job( - partial( - icmp_ping, - self._ip_address, - count=self._count, - timeout=ICMP_TIMEOUT, - id=async_get_next_ping_id(self.hass), - privileged=self._privileged, - ) + data = await async_ping( + self._ip_address, + count=self._count, + timeout=ICMP_TIMEOUT, + privileged=self._privileged, ) except NameLookupError: self.is_alive = False diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py index 62fca9123ba..9ca99db2419 100644 --- a/homeassistant/components/ping/const.py +++ b/homeassistant/components/ping/const.py @@ -15,7 +15,4 @@ PING_ATTEMPTS_COUNT = 3 DOMAIN = "ping" PLATFORMS = ["binary_sensor"] -PING_ID = "ping_id" PING_PRIVS = "ping_privs" -DEFAULT_START_ID = 129 -MAX_PING_ID = 65534 diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index d7d812d371d..b5acecf9314 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,12 +1,11 @@ """Tracks devices by sending a ICMP echo request (ping).""" import asyncio from datetime import timedelta -from functools import partial import logging import subprocess import sys -from icmplib import multiping +from icmplib import async_multiping import voluptuous as vol from homeassistant import const, util @@ -21,7 +20,6 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.process import kill_subprocess -from . import async_get_next_ping_id from .const import DOMAIN, ICMP_TIMEOUT, PING_ATTEMPTS_COUNT, PING_PRIVS, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -118,15 +116,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): async def async_update(now): """Update all the hosts on every interval time.""" - responses = await hass.async_add_executor_job( - partial( - multiping, - ip_to_dev_id.keys(), - count=PING_ATTEMPTS_COUNT, - timeout=ICMP_TIMEOUT, - privileged=privileged, - id=async_get_next_ping_id(hass, len(ip_to_dev_id)), - ) + responses = await async_multiping( + list(ip_to_dev_id), + count=PING_ATTEMPTS_COUNT, + timeout=ICMP_TIMEOUT, + privileged=privileged, ) _LOGGER.debug("Multiping responses: %s", responses) await asyncio.gather( diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 639a30a4fa0..d25d0fc731e 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,7 +3,7 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==2.1.1"], + "requirements": ["icmplib==3.0"], "quality_scale": "internal", "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0235499d0d9..447d4e238ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -816,7 +816,7 @@ ibm-watson==5.1.0 ibmiotf==0.3.4 # homeassistant.components.ping -icmplib==2.1.1 +icmplib==3.0 # homeassistant.components.network ifaddr==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21733c61b71..14b103584be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ hyperion-py==0.7.4 iaqualink==0.3.4 # homeassistant.components.ping -icmplib==2.1.1 +icmplib==3.0 # homeassistant.components.network ifaddr==0.1.7 diff --git a/tests/components/ping/test_init.py b/tests/components/ping/test_init.py index 3dfe193c4d5..05dc47e27d8 100644 --- a/tests/components/ping/test_init.py +++ b/tests/components/ping/test_init.py @@ -1,27 +1 @@ """Test ping id allocation.""" - -from homeassistant.components.ping import async_get_next_ping_id -from homeassistant.components.ping.const import ( - DEFAULT_START_ID, - DOMAIN, - MAX_PING_ID, - PING_ID, -) - - -async def test_async_get_next_ping_id(hass): - """Verify we allocate ping ids as expected.""" - hass.data[DOMAIN] = {PING_ID: DEFAULT_START_ID} - - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 - assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 3 - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 5 - - hass.data[DOMAIN][PING_ID] = MAX_PING_ID - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 1 - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 2 - - hass.data[DOMAIN][PING_ID] = MAX_PING_ID - assert async_get_next_ping_id(hass, 2) == DEFAULT_START_ID + 1 - assert async_get_next_ping_id(hass) == DEFAULT_START_ID + 3 From c057c9d9aba7140914ce30bcd54075d6b27fb58d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 2 Jun 2021 09:39:19 -0700 Subject: [PATCH 148/750] Add Hyperion camera feed (#46516) * Initial Hyperion camera. * Improve test coverage. * Minor state fixes. * Fix type annotation. * May rebase and updates (mostly typing). * Updates to use new camera typing improvements. * Use new support for returning None from async_get_mjpeg_stream . * Codereview feedback. * Lint: Use AsyncGenerator from collections.abc . * Update homeassistant/components/hyperion/camera.py Co-authored-by: Paulus Schoutsen --- homeassistant/components/hyperion/__init__.py | 3 +- homeassistant/components/hyperion/camera.py | 263 ++++++++++++++++++ homeassistant/components/hyperion/const.py | 2 + tests/components/hyperion/__init__.py | 13 +- tests/components/hyperion/test_camera.py | 211 ++++++++++++++ tests/components/hyperion/test_config_flow.py | 26 +- tests/components/hyperion/test_light.py | 8 +- tests/components/hyperion/test_switch.py | 2 +- 8 files changed, 508 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/hyperion/camera.py create mode 100644 tests/components/hyperion/test_camera.py diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 5baa86d6926..891f48e8738 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -9,6 +9,7 @@ from typing import Any, Callable, cast from awesomeversion import AwesomeVersion from hyperion import client, const as hyperion_const +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -34,7 +35,7 @@ from .const import ( SIGNAL_INSTANCE_REMOVE, ) -PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN, CAMERA_DOMAIN] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py new file mode 100644 index 00000000000..733a91c1c58 --- /dev/null +++ b/homeassistant/components/hyperion/camera.py @@ -0,0 +1,263 @@ +"""Switch platform for Hyperion.""" + +from __future__ import annotations + +import asyncio +import base64 +import binascii +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +import functools +import logging +from typing import Any, Callable + +from aiohttp import web +from hyperion import client +from hyperion.const import ( + KEY_IMAGE, + KEY_IMAGE_STREAM, + KEY_LEDCOLORS, + KEY_RESULT, + KEY_UPDATE, +) + +from homeassistant.components.camera import ( + DEFAULT_CONTENT_TYPE, + Camera, + async_get_still_stream, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) +from .const import ( + CONF_INSTANCE_CLIENTS, + DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, + NAME_SUFFIX_HYPERION_CAMERA, + SIGNAL_ENTITY_REMOVE, + TYPE_HYPERION_CAMERA, +) + +_LOGGER = logging.getLogger(__name__) + +IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64," + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +) -> bool: + """Set up a Hyperion platform from config entry.""" + entry_data = hass.data[DOMAIN][config_entry.entry_id] + server_id = config_entry.unique_id + + def camera_unique_id(instance_num: int) -> str: + """Return the camera unique_id.""" + assert server_id + return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_CAMERA) + + @callback + def instance_add(instance_num: int, instance_name: str) -> None: + """Add entities for a new Hyperion instance.""" + assert server_id + async_add_entities( + [ + HyperionCamera( + server_id, + instance_num, + instance_name, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ) + ] + ) + + @callback + def instance_remove(instance_num: int) -> None: + """Remove entities for an old Hyperion instance.""" + assert server_id + async_dispatcher_send( + hass, + SIGNAL_ENTITY_REMOVE.format( + camera_unique_id(instance_num), + ), + ) + + listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) + return True + + +# A note on Hyperion streaming semantics: +# +# Different Hyperion priorities behave different with regards to streaming. Colors will +# not stream (as there is nothing to stream). External grabbers (e.g. USB Capture) will +# stream what is being captured. Some effects (based on GIFs) will stream, others will +# not. In cases when streaming is not supported from a selected priority, there is no +# notification beyond the failure of new frames to arrive. + + +class HyperionCamera(Camera): + """ComponentBinarySwitch switch class.""" + + def __init__( + self, + server_id: str, + instance_num: int, + instance_name: str, + hyperion_client: client.HyperionClient, + ) -> None: + """Initialize the switch.""" + super().__init__() + + self._unique_id = get_hyperion_unique_id( + server_id, instance_num, TYPE_HYPERION_CAMERA + ) + self._name = f"{instance_name} {NAME_SUFFIX_HYPERION_CAMERA}".strip() + self._device_id = get_hyperion_device_id(server_id, instance_num) + self._instance_name = instance_name + self._client = hyperion_client + + self._image_cond = asyncio.Condition() + self._image: bytes | None = None + + # The number of open streams, when zero the stream is stopped. + self._image_stream_clients = 0 + + self._client_callbacks = { + f"{KEY_LEDCOLORS}-{KEY_IMAGE_STREAM}-{KEY_UPDATE}": self._update_imagestream + } + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the switch.""" + return self._name + + @property + def is_on(self) -> bool: + """Return true if the camera is on.""" + return self.available + + @property + def available(self) -> bool: + """Return server availability.""" + return bool(self._client.has_loaded_state) + + async def _update_imagestream(self, img: dict[str, Any] | None = None) -> None: + """Update Hyperion components.""" + if not img: + return + img_data = img.get(KEY_RESULT, {}).get(KEY_IMAGE) + if not img_data or not img_data.startswith(IMAGE_STREAM_JPG_SENTINEL): + return + async with self._image_cond: + try: + self._image = base64.b64decode( + img_data[len(IMAGE_STREAM_JPG_SENTINEL) :] + ) + except binascii.Error: + return + self._image_cond.notify_all() + + async def _async_wait_for_camera_image(self) -> bytes | None: + """Return a single camera image in a stream.""" + async with self._image_cond: + await self._image_cond.wait() + return self._image if self.available else None + + async def _start_image_streaming_for_client(self) -> bool: + """Start streaming for a client.""" + if ( + not self._image_stream_clients + and not await self._client.async_send_image_stream_start() + ): + return False + + self._image_stream_clients += 1 + self.is_streaming = True + self.async_write_ha_state() + return True + + async def _stop_image_streaming_for_client(self) -> None: + """Stop streaming for a client.""" + self._image_stream_clients -= 1 + + if not self._image_stream_clients: + await self._client.async_send_image_stream_stop() + self.is_streaming = False + self.async_write_ha_state() + + @asynccontextmanager + async def _image_streaming(self) -> AsyncGenerator: + """Async context manager to start/stop image streaming.""" + try: + yield await self._start_image_streaming_for_client() + finally: + await self._stop_image_streaming_for_client() + + async def async_camera_image(self) -> bytes | None: + """Return single camera image bytes.""" + async with self._image_streaming() as is_streaming: + if is_streaming: + return await self._async_wait_for_camera_image() + return None + + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse | None: + """Serve an HTTP MJPEG stream from the camera.""" + async with self._image_streaming() as is_streaming: + if is_streaming: + return await async_get_still_stream( + request, + self._async_wait_for_camera_image, + DEFAULT_CONTENT_TYPE, + 0.0, + ) + return None + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + assert self.hass + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._unique_id), + functools.partial(self.async_remove, force_remove=True), + ) + ) + + self._client.add_callbacks(self._client_callbacks) + + async def async_will_remove_from_hass(self) -> None: + """Cleanup prior to hass removal.""" + self._client.remove_callbacks(self._client_callbacks) + + @property + def device_info(self) -> DeviceInfo: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._instance_name, + "manufacturer": HYPERION_MANUFACTURER_NAME, + "model": HYPERION_MODEL_NAME, + } + + +CAMERA_TYPES = { + TYPE_HYPERION_CAMERA: HyperionCamera, +} diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 9deeba9d019..271618fb962 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -24,11 +24,13 @@ HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" NAME_SUFFIX_HYPERION_LIGHT = "" NAME_SUFFIX_HYPERION_PRIORITY_LIGHT = "Priority" NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component" +NAME_SUFFIX_HYPERION_CAMERA = "" SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}" SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}" SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}" +TYPE_HYPERION_CAMERA = "hyperion_camera" TYPE_HYPERION_LIGHT = "hyperion_light" TYPE_HYPERION_PRIORITY_LIGHT = "hyperion_priority_light" TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch" diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index ac77a5a0407..0789a344ec6 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -125,7 +125,7 @@ def add_test_config_entry( options: dict[str, Any] | None = None, ) -> ConfigEntry: """Add a test config entry.""" - config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + config_entry: MockConfigEntry = MockConfigEntry( entry_id=TEST_CONFIG_ENTRY_ID, domain=DOMAIN, data=data @@ -137,7 +137,7 @@ def add_test_config_entry( unique_id=TEST_SYSINFO_ID, options=options or TEST_CONFIG_ENTRY_OPTIONS, ) - config_entry.add_to_hass(hass) # type: ignore[no-untyped-call] + config_entry.add_to_hass(hass) return config_entry @@ -187,3 +187,12 @@ def register_test_entity( suggested_object_id=entity_id, disabled_by=None, ) + + +async def async_call_registered_callback( + client: AsyncMock, key: str, *args: Any, **kwargs: Any +) -> None: + """Call Hyperion entity callbacks that were registered with the client.""" + for call in client.add_callbacks.call_args_list: + if key in call[0][0]: + await call[0][0][key](*args, **kwargs) diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py new file mode 100644 index 00000000000..daed1ec2cc9 --- /dev/null +++ b/tests/components/hyperion/test_camera.py @@ -0,0 +1,211 @@ +"""Tests for the Hyperion integration.""" +from __future__ import annotations + +import asyncio +import base64 +from collections.abc import Awaitable +import logging +from typing import Callable +from unittest.mock import AsyncMock, Mock, patch + +from aiohttp import web +import pytest + +from homeassistant.components.camera import ( + DEFAULT_CONTENT_TYPE, + DOMAIN as CAMERA_DOMAIN, + async_get_image, + async_get_mjpeg_stream, +) +from homeassistant.components.hyperion import get_hyperion_device_id +from homeassistant.components.hyperion.const import ( + DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, + TYPE_HYPERION_CAMERA, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import ( + TEST_CONFIG_ENTRY_ID, + TEST_INSTANCE, + TEST_INSTANCE_1, + TEST_SYSINFO_ID, + async_call_registered_callback, + create_mock_client, + register_test_entity, + setup_test_config_entry, +) + +_LOGGER = logging.getLogger(__name__) +TEST_CAMERA_ENTITY_ID = "camera.test_instance_1" +TEST_IMAGE_DATA = "TEST DATA" +TEST_IMAGE_UPDATE = { + "command": "ledcolors-imagestream-update", + "result": { + "image": "data:image/jpg;base64," + + base64.b64encode(TEST_IMAGE_DATA.encode()).decode("ascii"), + }, + "success": True, +} + + +async def test_camera_setup(hass: HomeAssistant) -> None: + """Test turning the light on.""" + client = create_mock_client() + + await setup_test_config_entry(hass, hyperion_client=client) + + # Verify switch is on (as per TEST_COMPONENTS above). + entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state + assert entity_state.state == "idle" + + +async def test_camera_image(hass: HomeAssistant) -> None: + """Test retrieving a single camera image.""" + client = create_mock_client() + client.async_send_image_stream_start = AsyncMock(return_value=True) + client.async_send_image_stream_stop = AsyncMock(return_value=True) + + await setup_test_config_entry(hass, hyperion_client=client) + + get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID) + image_stream_update_coro = async_call_registered_callback( + client, "ledcolors-imagestream-update", TEST_IMAGE_UPDATE + ) + result = await asyncio.gather(get_image_coro, image_stream_update_coro) + + assert client.async_send_image_stream_start.called + assert client.async_send_image_stream_stop.called + assert result[0].content == TEST_IMAGE_DATA.encode() + + +async def test_camera_invalid_image(hass: HomeAssistant) -> None: + """Test retrieving a single invalid camera image.""" + client = create_mock_client() + client.async_send_image_stream_start = AsyncMock(return_value=True) + client.async_send_image_stream_stop = AsyncMock(return_value=True) + + await setup_test_config_entry(hass, hyperion_client=client) + + get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0) + image_stream_update_coro = async_call_registered_callback( + client, "ledcolors-imagestream-update", None + ) + with pytest.raises(HomeAssistantError): + await asyncio.gather(get_image_coro, image_stream_update_coro) + + get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0) + image_stream_update_coro = async_call_registered_callback( + client, "ledcolors-imagestream-update", {"garbage": 1} + ) + with pytest.raises(HomeAssistantError): + await asyncio.gather(get_image_coro, image_stream_update_coro) + + get_image_coro = async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0) + image_stream_update_coro = async_call_registered_callback( + client, + "ledcolors-imagestream-update", + {"result": {"image": "data:image/jpg;base64,FOO"}}, + ) + with pytest.raises(HomeAssistantError): + await asyncio.gather(get_image_coro, image_stream_update_coro) + + +async def test_camera_image_failed_start_stream_call(hass: HomeAssistant) -> None: + """Test retrieving a single camera image with failed start stream call.""" + client = create_mock_client() + client.async_send_image_stream_start = AsyncMock(return_value=False) + + await setup_test_config_entry(hass, hyperion_client=client) + + with pytest.raises(HomeAssistantError): + await async_get_image(hass, TEST_CAMERA_ENTITY_ID, timeout=0) + + assert client.async_send_image_stream_start.called + assert not client.async_send_image_stream_stop.called + + +async def test_camera_stream(hass: HomeAssistant) -> None: + """Test retrieving a camera stream.""" + client = create_mock_client() + client.async_send_image_stream_start = AsyncMock(return_value=True) + client.async_send_image_stream_stop = AsyncMock(return_value=True) + + request = Mock() + + async def fake_get_still_stream( + in_request: web.Request, + callback: Callable[[], Awaitable[bytes | None]], + content_type: str, + interval: float, + ) -> bytes | None: + assert request == in_request + assert content_type == DEFAULT_CONTENT_TYPE + assert interval == 0.0 + return await callback() + + await setup_test_config_entry(hass, hyperion_client=client) + + with patch( + "homeassistant.components.hyperion.camera.async_get_still_stream", + ) as fake: + fake.side_effect = fake_get_still_stream + + get_stream_coro = async_get_mjpeg_stream(hass, request, TEST_CAMERA_ENTITY_ID) + image_stream_update_coro = async_call_registered_callback( + client, "ledcolors-imagestream-update", TEST_IMAGE_UPDATE + ) + result = await asyncio.gather(get_stream_coro, image_stream_update_coro) + + assert client.async_send_image_stream_start.called + assert client.async_send_image_stream_stop.called + assert result[0] == TEST_IMAGE_DATA.encode() + + +async def test_camera_stream_failed_start_stream_call(hass: HomeAssistant) -> None: + """Test retrieving a camera stream with failed start stream call.""" + client = create_mock_client() + client.async_send_image_stream_start = AsyncMock(return_value=False) + + await setup_test_config_entry(hass, hyperion_client=client) + + request = Mock() + assert not await async_get_mjpeg_stream(hass, request, TEST_CAMERA_ENTITY_ID) + + assert client.async_send_image_stream_start.called + assert not client.async_send_image_stream_stop.called + + +async def test_device_info(hass: HomeAssistant) -> None: + """Verify device information includes expected details.""" + client = create_mock_client() + + register_test_entity( + hass, + CAMERA_DOMAIN, + TYPE_HYPERION_CAMERA, + TEST_CAMERA_ENTITY_ID, + ) + await setup_test_config_entry(hass, hyperion_client=client) + + device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + assert device + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.identifiers == {(DOMAIN, device_id)} + assert device.manufacturer == HYPERION_MANUFACTURER_NAME + assert device.model == HYPERION_MODEL_NAME + assert device.name == TEST_INSTANCE_1["friendly_name"] + + entity_registry = await er.async_get_registry(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + assert TEST_CAMERA_ENTITY_ID in entities_from_device diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index d8b12e3c72b..7260d589e71 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Awaitable +from collections.abc import Awaitable +from typing import Any from unittest.mock import AsyncMock, Mock, patch from hyperion import const @@ -26,6 +27,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from . import ( TEST_AUTH_REQUIRED_RESP, @@ -100,7 +102,7 @@ TEST_SSDP_SERVICE_INFO = { async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry: """Add a test Hyperion entity to hass.""" - entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] + entry: MockConfigEntry = MockConfigEntry( entry_id=TEST_CONFIG_ENTRY_ID, domain=DOMAIN, unique_id=TEST_SYSINFO_ID, @@ -111,7 +113,7 @@ async def _create_mock_entry(hass: HomeAssistant) -> MockConfigEntry: "instance": TEST_INSTANCE, }, ) - entry.add_to_hass(hass) # type: ignore[no-untyped-call] + entry.add_to_hass(hass) # Setup client = create_mock_client() @@ -138,7 +140,7 @@ async def _init_flow( async def _configure_flow( - hass: HomeAssistant, result: dict, user_input: dict[str, Any] | None = None + hass: HomeAssistant, result: FlowResult, user_input: dict[str, Any] | None = None ) -> Any: """Provide input to a flow.""" user_input = user_input or {} @@ -419,6 +421,11 @@ async def test_auth_create_token_approval_declined_task_canceled( class CanceledAwaitableMock(AsyncMock): """A canceled awaitable mock.""" + def __init__(self): + super().__init__() + self.done = Mock(return_value=False) + self.cancel = Mock() + def __await__(self) -> None: raise asyncio.CancelledError @@ -435,20 +442,15 @@ async def test_auth_create_token_approval_declined_task_canceled( ), patch( "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", return_value=TEST_AUTH_ID, - ), patch.object( - hass, "async_create_task", side_effect=create_task ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) assert result["step_id"] == "create_token" - result = await _configure_flow(hass, result) - assert result["step_id"] == "create_token_external" - - # Leave the task running, to ensure it is canceled. - mock_task.done = Mock(return_value=False) - mock_task.cancel = Mock() + with patch.object(hass, "async_create_task", side_effect=create_task): + result = await _configure_flow(hass, result) + assert result["step_id"] == "create_token_external" result = await _configure_flow(hass, result) diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 0c6b2cf41df..866b1b1b1a8 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -862,7 +862,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert client.async_client_disconnect.call_count == 2 -async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: """Test warning on old version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7") @@ -871,7 +871,7 @@ async def test_version_log_warning(caplog, hass: HomeAssistant) -> None: # type assert "Please consider upgrading" in caplog.text -async def test_version_no_log_warning(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_version_no_log_warning(caplog, hass: HomeAssistant) -> None: """Test no warning on acceptable version.""" client = create_mock_client() client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9") @@ -1359,7 +1359,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistant) -> None: assert not updated_entry.disabled await hass.async_block_till_done() - async_fire_time_changed( # type: ignore[no-untyped-call] + async_fire_time_changed( hass, dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) @@ -1369,7 +1369,7 @@ async def test_lights_can_be_enabled(hass: HomeAssistant) -> None: assert entity_state -async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_deprecated_effect_names(caplog, hass: HomeAssistant) -> None: """Test deprecated effects function and issue a warning.""" client = create_mock_client() client.async_send_clear = AsyncMock(return_value=True) diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 2367ad96133..e88db516eff 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -213,7 +213,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistant) -> None: assert not updated_entry.disabled await hass.async_block_till_done() - async_fire_time_changed( # type: ignore[no-untyped-call] + async_fire_time_changed( hass, dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) From 132ee972bd27b53b727d730cf7f1c1a1a18ad2f1 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Wed, 2 Jun 2021 22:25:15 +0300 Subject: [PATCH 149/750] Add media_player.group (#38855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add media group * Minor improvements * Use the async api for all methods * Improve type hints * Add missing methods * Add tests * Rename HomeAssistantType —> HomeAssistant * Add more tests * Fix unknown state * Make some callbacks * Add more tests * Fix unknown state properly * Fix names for callbacks * Fix stop service test * Improve tests --- .../components/group/media_player.py | 411 ++++++++++++++ tests/components/group/test_media_player.py | 516 ++++++++++++++++++ 2 files changed, 927 insertions(+) create mode 100644 homeassistant/components/group/media_player.py create mode 100644 tests/components/group/test_media_player.py diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py new file mode 100644 index 00000000000..568812fd6e0 --- /dev/null +++ b/homeassistant/components/group/media_player.py @@ -0,0 +1,411 @@ +"""This platform allows several media players to be grouped into one media player.""" +from __future__ import annotations + +from typing import Any, Callable + +import voluptuous as vol + +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN, + PLATFORM_SCHEMA, + SERVICE_CLEAR_PLAYLIST, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, + SERVICE_MEDIA_STOP, + SERVICE_PLAY_MEDIA, + SERVICE_SHUFFLE_SET, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, + MediaPlayerEntity, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType + +KEY_CLEAR_PLAYLIST = "clear_playlist" +KEY_ON_OFF = "on_off" +KEY_PAUSE_PLAY_STOP = "play" +KEY_PLAY_MEDIA = "play_media" +KEY_SHUFFLE = "shuffle" +KEY_SEEK = "seek" +KEY_TRACKS = "tracks" +KEY_VOLUME = "volume" + +DEFAULT_NAME = "Media Group" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + } +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Media Group platform.""" + async_add_entities([MediaGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + + +class MediaGroup(MediaPlayerEntity): + """Representation of a Media Group.""" + + def __init__(self, name: str, entities: list[str]) -> None: + """Initialize a Media Group entity.""" + self._name = name + self._state: str | None = None + self._supported_features: int = 0 + + self._entities = entities + self._features: dict[str, set[str]] = { + KEY_CLEAR_PLAYLIST: set(), + KEY_ON_OFF: set(), + KEY_PAUSE_PLAY_STOP: set(), + KEY_PLAY_MEDIA: set(), + KEY_SHUFFLE: set(), + KEY_SEEK: set(), + KEY_TRACKS: set(), + KEY_VOLUME: set(), + } + + @callback + def async_on_state_change(self, event: EventType) -> None: + """Update supported features and state when a new state is received.""" + self.async_set_context(event.context) + self.async_update_supported_features( + event.data.get("entity_id"), event.data.get("new_state") # type: ignore + ) + self.async_update_state() + + @callback + def async_update_supported_features( + self, + entity_id: str, + new_state: State | None, + ) -> None: + """Update dictionaries with supported features.""" + if not new_state: + for players in self._features.values(): + players.discard(entity_id) + return + + new_features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if new_features & SUPPORT_CLEAR_PLAYLIST: + self._features[KEY_CLEAR_PLAYLIST].add(entity_id) + else: + self._features[KEY_CLEAR_PLAYLIST].discard(entity_id) + if new_features & (SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK): + self._features[KEY_TRACKS].add(entity_id) + else: + self._features[KEY_TRACKS].discard(entity_id) + if new_features & (SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP): + self._features[KEY_PAUSE_PLAY_STOP].add(entity_id) + else: + self._features[KEY_PAUSE_PLAY_STOP].discard(entity_id) + if new_features & SUPPORT_PLAY_MEDIA: + self._features[KEY_PLAY_MEDIA].add(entity_id) + else: + self._features[KEY_PLAY_MEDIA].discard(entity_id) + if new_features & SUPPORT_SEEK: + self._features[KEY_SEEK].add(entity_id) + else: + self._features[KEY_SEEK].discard(entity_id) + if new_features & SUPPORT_SHUFFLE_SET: + self._features[KEY_SHUFFLE].add(entity_id) + else: + self._features[KEY_SHUFFLE].discard(entity_id) + if new_features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF): + self._features[KEY_ON_OFF].add(entity_id) + else: + self._features[KEY_ON_OFF].discard(entity_id) + if new_features & ( + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP + ): + self._features[KEY_VOLUME].add(entity_id) + else: + self._features[KEY_VOLUME].discard(entity_id) + + async def async_added_to_hass(self) -> None: + """Register listeners.""" + for entity_id in self._entities: + new_state = self.hass.states.get(entity_id) + self.async_update_supported_features(entity_id, new_state) + async_track_state_change_event( + self.hass, self._entities, self.async_on_state_change + ) + self.async_update_state() + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def state(self) -> str | None: + """Return the state of the media group.""" + return self._state + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def should_poll(self) -> bool: + """No polling needed for a media group.""" + return False + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes for the media group.""" + return {ATTR_ENTITY_ID: self._entities} + + async def async_clear_playlist(self) -> None: + """Clear players playlist.""" + data = {ATTR_ENTITY_ID: self._features[KEY_CLEAR_PLAYLIST]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_PLAYLIST, + data, + context=self._context, + ) + + async def async_media_next_track(self) -> None: + """Send next track command.""" + data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + data, + context=self._context, + ) + + async def async_media_pause(self) -> None: + """Send pause command.""" + data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_PAUSE, + data, + context=self._context, + ) + + async def async_media_play(self) -> None: + """Send play command.""" + data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_PLAY, + data, + context=self._context, + ) + + async def async_media_previous_track(self) -> None: + """Send previous track command.""" + data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + data, + context=self._context, + ) + + async def async_media_seek(self, position: int) -> None: + """Send seek command.""" + data = { + ATTR_ENTITY_ID: self._features[KEY_SEEK], + ATTR_MEDIA_SEEK_POSITION: position, + } + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_SEEK, + data, + context=self._context, + ) + + async def async_media_stop(self) -> None: + """Send stop command.""" + data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_MEDIA_STOP, + data, + context=self._context, + ) + + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + data = { + ATTR_ENTITY_ID: self._features[KEY_VOLUME], + ATTR_MEDIA_VOLUME_MUTED: mute, + } + await self.hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_MUTE, + data, + context=self._context, + ) + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + data = { + ATTR_ENTITY_ID: self._features[KEY_PLAY_MEDIA], + ATTR_MEDIA_CONTENT_ID: media_id, + ATTR_MEDIA_CONTENT_TYPE: media_type, + } + await self.hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + data, + context=self._context, + ) + + async def async_set_shuffle(self, shuffle: bool) -> None: + """Enable/disable shuffle mode.""" + data = { + ATTR_ENTITY_ID: self._features[KEY_SHUFFLE], + ATTR_MEDIA_SHUFFLE: shuffle, + } + await self.hass.services.async_call( + DOMAIN, + SERVICE_SHUFFLE_SET, + data, + context=self._context, + ) + + async def async_turn_on(self) -> None: + """Forward the turn_on command to all media in the media group.""" + data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + data, + context=self._context, + ) + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level(s).""" + data = { + ATTR_ENTITY_ID: self._features[KEY_VOLUME], + ATTR_MEDIA_VOLUME_LEVEL: volume, + } + await self.hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + data, + context=self._context, + ) + + async def async_turn_off(self) -> None: + """Forward the turn_off command to all media in the media group.""" + data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]} + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + data, + context=self._context, + ) + + async def async_volume_up(self) -> None: + """Turn volume up for media player(s).""" + for entity in self._features[KEY_VOLUME]: + volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore + if volume_level < 1: + await self.async_set_volume_level(min(1, volume_level + 0.1)) + + async def async_volume_down(self) -> None: + """Turn volume down for media player(s).""" + for entity in self._features[KEY_VOLUME]: + volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore + if volume_level > 0: + await self.async_set_volume_level(max(0, volume_level - 0.1)) + + @callback + def async_update_state(self) -> None: + """Query all members and determine the media group state.""" + states = [self.hass.states.get(entity) for entity in self._entities] + states_values = [state.state for state in states if state is not None] + off_values = STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN + + if states_values: + if states_values.count(states_values[0]) == len(states_values): + self._state = states_values[0] + elif any(state for state in states_values if state not in off_values): + self._state = STATE_ON + else: + self._state = STATE_OFF + else: + self._state = None + + supported_features = 0 + supported_features |= ( + SUPPORT_CLEAR_PLAYLIST if self._features[KEY_CLEAR_PLAYLIST] else 0 + ) + supported_features |= ( + SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK + if self._features[KEY_TRACKS] + else 0 + ) + supported_features |= ( + SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP + if self._features[KEY_PAUSE_PLAY_STOP] + else 0 + ) + supported_features |= ( + SUPPORT_PLAY_MEDIA if self._features[KEY_PLAY_MEDIA] else 0 + ) + supported_features |= SUPPORT_SEEK if self._features[KEY_SEEK] else 0 + supported_features |= SUPPORT_SHUFFLE_SET if self._features[KEY_SHUFFLE] else 0 + supported_features |= ( + SUPPORT_TURN_ON | SUPPORT_TURN_OFF if self._features[KEY_ON_OFF] else 0 + ) + supported_features |= ( + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP + if self._features[KEY_VOLUME] + else 0 + ) + + self._supported_features = supported_features + self.async_write_ha_state() diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py new file mode 100644 index 00000000000..5dd5e4225cc --- /dev/null +++ b/tests/components/group/test_media_player.py @@ -0,0 +1,516 @@ +"""The tests for the Media group platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.group import DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_SHUFFLE, + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN as MEDIA_DOMAIN, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_SEEK, + SERVICE_PLAY_MEDIA, + SERVICE_SHUFFLE_SET, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_SET, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_SEEK, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_MUTED, + SERVICE_CLEAR_PLAYLIST, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component + + +@pytest.fixture(name="mock_media_seek") +def media_player_media_seek_fixture(): + """Mock demo YouTube player media seek.""" + with patch( + "homeassistant.components.demo.media_player.DemoYoutubePlayer.media_seek", + autospec=True, + ) as seek: + yield seek + + +async def test_default_state(hass): + """Test media group default state.""" + hass.states.async_set("media_player.player_1", "on") + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: { + "platform": DOMAIN, + "entities": ["media_player.player_1", "media_player.player_2"], + "name": "Media group", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("media_player.media_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "media_player.player_1", + "media_player.player_2", + ] + + +async def test_state_reporting(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: { + "platform": DOMAIN, + "entities": ["media_player.player_1", "media_player.player_2"], + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN + + hass.states.async_set("media_player.player_1", STATE_ON) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_ON + + hass.states.async_set("media_player.player_1", STATE_ON) + hass.states.async_set("media_player.player_2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_ON + + hass.states.async_set("media_player.player_1", STATE_OFF) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF + + hass.states.async_set("media_player.player_1", STATE_UNAVAILABLE) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE + + +async def test_supported_features(hass): + """Test supported features reporting.""" + pause_play_stop = SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP + play_media = SUPPORT_PLAY_MEDIA + volume = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP + + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: { + "platform": DOMAIN, + "entities": ["media_player.player_1", "media_player.player_2"], + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set( + "media_player.player_1", STATE_ON, {ATTR_SUPPORTED_FEATURES: 0} + ) + await hass.async_block_till_done() + state = hass.states.get("media_player.media_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + hass.states.async_set( + "media_player.player_1", + STATE_ON, + {ATTR_SUPPORTED_FEATURES: pause_play_stop}, + ) + await hass.async_block_till_done() + state = hass.states.get("media_player.media_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == pause_play_stop + + hass.states.async_set( + "media_player.player_2", + STATE_OFF, + {ATTR_SUPPORTED_FEATURES: play_media | volume}, + ) + await hass.async_block_till_done() + state = hass.states.get("media_player.media_group") + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == pause_play_stop | play_media | volume + ) + + hass.states.async_set( + "media_player.player_2", STATE_OFF, {ATTR_SUPPORTED_FEATURES: play_media} + ) + await hass.async_block_till_done() + state = hass.states.get("media_player.media_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == pause_play_stop | play_media + + +async def test_service_calls(hass, mock_media_seek): + """Test service calls.""" + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "media_player.bedroom", + "media_player.kitchen", + "media_player.living_room", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("media_player.media_group").state == STATE_PLAYING + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_OFF + assert hass.states.get("media_player.kitchen").state == STATE_OFF + assert hass.states.get("media_player.living_room").state == STATE_OFF + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_PLAYING + assert hass.states.get("media_player.kitchen").state == STATE_PLAYING + assert hass.states.get("media_player.living_room").state == STATE_PLAYING + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_PAUSE, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_PAUSED + assert hass.states.get("media_player.kitchen").state == STATE_PAUSED + assert hass.states.get("media_player.living_room").state == STATE_PAUSED + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_PLAYING + assert hass.states.get("media_player.kitchen").state == STATE_PLAYING + assert hass.states.get("media_player.living_room").state == STATE_PLAYING + + # ATTR_MEDIA_TRACK is not supported by bedroom and living_room players + assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 1 + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 2 + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_TRACK] == 1 + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.media_group", + ATTR_MEDIA_CONTENT_TYPE: "some_type", + ATTR_MEDIA_CONTENT_ID: "some_id", + }, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_CONTENT_ID] + == "some_id" + ) + # media_player.kitchen is skipped because it always returns "bounzz-1" + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_CONTENT_ID] + == "some_id" + ) + + state = hass.states.get("media_player.media_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] & SUPPORT_SEEK + assert not mock_media_seek.called + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_SEEK, + { + ATTR_ENTITY_ID: "media_player.media_group", + ATTR_MEDIA_SEEK_POSITION: 100, + }, + ) + await hass.async_block_till_done() + assert mock_media_seek.called + + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1 + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1 + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 1 + ) + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: "media_player.media_group", + ATTR_MEDIA_VOLUME_LEVEL: 0.5, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_VOLUME_UP, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.6 + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.6 + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.6 + ) + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_VOLUME_DOWN, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_LEVEL] + == 0.5 + ) + + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_MUTED] + is False + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_MUTED] + is False + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_MUTED] + is False + ) + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: "media_player.media_group", ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_VOLUME_MUTED] + is True + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_VOLUME_MUTED] + is True + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_VOLUME_MUTED] + is True + ) + + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_SHUFFLE] is False + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_SHUFFLE] is False + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_SHUFFLE] + is False + ) + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_SHUFFLE_SET, + {ATTR_ENTITY_ID: "media_player.media_group", ATTR_MEDIA_SHUFFLE: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + hass.states.get("media_player.bedroom").attributes[ATTR_MEDIA_SHUFFLE] is True + ) + assert ( + hass.states.get("media_player.kitchen").attributes[ATTR_MEDIA_SHUFFLE] is True + ) + assert ( + hass.states.get("media_player.living_room").attributes[ATTR_MEDIA_SHUFFLE] + is True + ) + + assert hass.states.get("media_player.bedroom").state == STATE_PLAYING + assert hass.states.get("media_player.kitchen").state == STATE_PLAYING + assert hass.states.get("media_player.living_room").state == STATE_PLAYING + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_CLEAR_PLAYLIST, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + # SERVICE_CLEAR_PLAYLIST is not supported by bedroom and living_room players + assert hass.states.get("media_player.kitchen").state == STATE_OFF + + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: "media_player.kitchen"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_PLAYING + assert hass.states.get("media_player.kitchen").state == STATE_PLAYING + assert hass.states.get("media_player.living_room").state == STATE_PLAYING + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: "media_player.media_group"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_OFF + assert hass.states.get("media_player.kitchen").state == STATE_OFF + assert hass.states.get("media_player.living_room").state == STATE_OFF + + +async def test_nested_group(hass): + """Test nested media group.""" + hass.states.async_set("media_player.player_1", "on") + await async_setup_component( + hass, + MEDIA_DOMAIN, + { + MEDIA_DOMAIN: [ + { + "platform": DOMAIN, + "entities": ["media_player.group_1"], + "name": "Nested Group", + }, + { + "platform": DOMAIN, + "entities": ["media_player.player_1", "media_player.player_2"], + "name": "Group 1", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("media_player.group_1") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "media_player.player_1", + "media_player.player_2", + ] + + state = hass.states.get("media_player.nested_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == ["media_player.group_1"] From 2222a121f48e983ca4c668d75e417c653a5d6732 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 2 Jun 2021 22:09:22 +0200 Subject: [PATCH 150/750] Add support for fan speed percentage and preset modes to google_assistant integration (#50283) * support relative fan speeds * fan preset modes * improve tests * Revert relative speed code report zero percentage --- .../components/google_assistant/trait.py | 68 +++++++++----- tests/components/google_assistant/__init__.py | 15 ++- .../components/google_assistant/test_trait.py | 92 +++++++++++++++++++ 3 files changed, 149 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8286e527159..d7cd55b7f80 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -121,6 +121,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = ( COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode" COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock" COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed" +COMMAND_FANSPEEDRELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative" COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes" COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput" COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput" @@ -1276,10 +1277,9 @@ class FanSpeedTrait(_Trait): reversible = False if domain == fan.DOMAIN: + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) for mode in modes: - if mode not in self.speed_synonyms: - continue speed = { "speed_name": mode, "speed_values": [ @@ -1321,6 +1321,7 @@ class FanSpeedTrait(_Trait): if speed is not None: response["on"] = speed != fan.SPEED_OFF response["currentFanSpeedSetting"] = speed + if percent is not None: response["currentFanSpeedPercent"] = percent return response @@ -1369,6 +1370,7 @@ class ModesTrait(_Trait): commands = [COMMAND_MODES] SYNONYMS = { + "preset mode": ["preset mode", "mode", "preset"], "sound mode": ["sound mode", "effects"], "option": ["option", "setting", "mode", "value"], } @@ -1376,6 +1378,9 @@ class ModesTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" + if domain == fan.DOMAIN and features & fan.SUPPORT_PRESET_MODE: + return True + if domain == input_select.DOMAIN: return True @@ -1419,6 +1424,7 @@ class ModesTrait(_Trait): modes = [] for domain, attr, name in ( + (fan.DOMAIN, fan.ATTR_PRESET_MODES, "preset mode"), (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"), (input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"), (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"), @@ -1445,7 +1451,10 @@ class ModesTrait(_Trait): response = {} mode_settings = {} - if self.state.domain == media_player.DOMAIN: + if self.state.domain == fan.DOMAIN: + if fan.ATTR_PRESET_MODES in attrs: + mode_settings["preset mode"] = attrs.get(fan.ATTR_PRESET_MODE) + elif self.state.domain == media_player.DOMAIN: if media_player.ATTR_SOUND_MODE_LIST in attrs: mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) elif self.state.domain == input_select.DOMAIN: @@ -1466,8 +1475,22 @@ class ModesTrait(_Trait): """Execute a SetModes command.""" settings = params.get("updateModeSettings") + if self.state.domain == fan.DOMAIN: + preset_mode = settings["preset mode"] + await self.hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: self.state.entity_id, + fan.ATTR_PRESET_MODE: preset_mode, + }, + blocking=True, + context=data.context, + ) + return + if self.state.domain == input_select.DOMAIN: - option = params["updateModeSettings"]["option"] + option = settings["option"] await self.hass.services.async_call( input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION, @@ -1508,26 +1531,25 @@ class ModesTrait(_Trait): ) return - if self.state.domain != media_player.DOMAIN: - _LOGGER.info( - "Received an Options command for unrecognised domain %s", - self.state.domain, - ) - return + if self.state.domain == media_player.DOMAIN: + sound_mode = settings.get("sound mode") + if sound_mode: + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOUND_MODE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_SOUND_MODE: sound_mode, + }, + blocking=True, + context=data.context, + ) - sound_mode = settings.get("sound mode") - - if sound_mode: - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_SELECT_SOUND_MODE, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_SOUND_MODE: sound_mode, - }, - blocking=True, - context=data.context, - ) + _LOGGER.info( + "Received an Options command for unrecognised domain %s", + self.state.domain, + ) + return @register_trait diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 123ca120243..459b5bcadfc 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -247,14 +247,20 @@ DEMO_DEVICES = [ { "id": "fan.living_room_fan", "name": {"name": "Living Room Fan"}, - "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"], + "traits": [ + "action.devices.traits.FanSpeed", + "action.devices.traits.OnOff", + ], "type": "action.devices.types.FAN", "willReportState": False, }, { "id": "fan.ceiling_fan", "name": {"name": "Ceiling Fan"}, - "traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"], + "traits": [ + "action.devices.traits.FanSpeed", + "action.devices.traits.OnOff", + ], "type": "action.devices.types.FAN", "willReportState": False, }, @@ -275,7 +281,10 @@ DEMO_DEVICES = [ { "id": "fan.preset_only_limited_fan", "name": {"name": "Preset Only Limited Fan"}, - "traits": ["action.devices.traits.OnOff"], + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Modes", + ], "type": "action.devices.types.FAN", "willReportState": False, }, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index c3678e7f99a..46d81443a05 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1429,6 +1429,7 @@ async def test_fan_speed(hass): ], "speed": "low", "percentage": 33, + "percentage_step": 1.0, }, ), BASIC_CONFIG, @@ -1951,6 +1952,97 @@ async def test_sound_modes(hass): } +async def test_preset_modes(hass): + """Test Mode trait for fan preset modes.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None + assert trait.ModesTrait.supported(fan.DOMAIN, fan.SUPPORT_PRESET_MODE, None, None) + + trt = trait.ModesTrait( + hass, + State( + "fan.living_room", + STATE_ON, + attributes={ + fan.ATTR_PRESET_MODES: ["auto", "whoosh"], + fan.ATTR_PRESET_MODE: "auto", + ATTR_SUPPORTED_FEATURES: fan.SUPPORT_PRESET_MODE, + }, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "preset mode", + "name_values": [ + {"name_synonym": ["preset mode", "mode", "preset"], "lang": "en"} + ], + "settings": [ + { + "setting_name": "auto", + "setting_values": [{"setting_synonym": ["auto"], "lang": "en"}], + }, + { + "setting_name": "whoosh", + "setting_values": [ + {"setting_synonym": ["whoosh"], "lang": "en"} + ], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"preset mode": "auto"}, + "on": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, + params={"updateModeSettings": {"preset mode": "auto"}}, + ) + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE) + await trt.execute( + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"preset mode": "auto"}}, + {}, + ) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "fan.living_room", + "preset_mode": "auto", + } + + +async def test_traits_unknown_domains(hass, caplog): + """Test Mode trait for unsupported domain.""" + trt = trait.ModesTrait( + hass, + State( + "switch.living_room", + STATE_ON, + ), + BASIC_CONFIG, + ) + + assert trt.supported("not_supported_domain", False, None, None) is False + await trt.execute( + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {}}, + {}, + ) + assert "Received an Options command for unrecognised domain" in caplog.text + caplog.clear() + + async def test_openclose_cover(hass): """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None From 7f6e20dcbcca0fb9754029830c850cf457a79618 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 3 Jun 2021 00:26:58 +0000 Subject: [PATCH 151/750] [ci skip] Translation update --- .../components/cover/translations/nl.json | 4 ++-- .../components/deconz/translations/nl.json | 14 +++++++------- homeassistant/components/nest/translations/nl.json | 2 +- .../components/samsungtv/translations/cs.json | 2 +- .../components/somfy_mylink/translations/nl.json | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index 8b1ca3c3500..c3998187c4f 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -6,7 +6,7 @@ "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", + "set_tilt_position": "Stel de kantelpositie van {entity_name} in", "stop": "Stop {entity_name}" }, "condition_type": { @@ -35,5 +35,5 @@ "stopped": "Gestopt" } }, - "title": "Bedekking" + "title": "Rolluik" } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 29031a7d731..9c83fb806e8 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "Bridge is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", - "no_bridges": "Geen deCONZ apparaten ontdekt", + "no_bridges": "Geen deCONZ bridges ontdekt", "no_hardware_available": "Geen radiohardware aangesloten op deCONZ", - "not_deconz_bridge": "Dit is geen deCONZ bridge", + "not_deconz_bridge": "Geen deCONZ bridge", "updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres" }, "error": { @@ -67,7 +67,7 @@ "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", - "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quadruple_press": "\" {subtype} \" knop vier keer aangeklikt", "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", "remote_button_rotated": "Knop gedraaid \" {subtype} \"", "remote_button_rotated_fast": "Knop is snel gedraaid \" {subtype} \"", @@ -76,13 +76,13 @@ "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", "remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt", - "remote_double_tap_any_side": "Apparaat dubbel getikt aan elke kant", + "remote_double_tap_any_side": "Apparaat dubbel getikt op willekeurige zijde", "remote_falling": "Apparaat in vrije val", - "remote_flip_180_degrees": "Apparaat 180 graden omgedraaid", - "remote_flip_90_degrees": "Apparaat 90 graden omgedraaid", + "remote_flip_180_degrees": "Apparaat 180 graden gedraaid", + "remote_flip_90_degrees": "Apparaat 90 graden gedraaid", "remote_gyro_activated": "Apparaat geschud", "remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog", - "remote_moved_any_side": "Apparaat gedraaid met elke kant naar boven", + "remote_moved_any_side": "Apparaat gedraaid met willekeurige zijde boven", "remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".", "remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".", "remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"", diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 53066e9e720..a849b76f1c9 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -36,7 +36,7 @@ "title": "Kies een authenticatie methode" }, "reauth_confirm": { - "description": "De Nest-integratie moet je account opnieuw verifi\u00ebren", + "description": "De Nest-integratie moet uw account opnieuw verifi\u00ebren", "title": "Verifieer de integratie opnieuw" } } diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json index f141dc3a09b..4453c7d227e 100644 --- a/homeassistant/components/samsungtv/translations/cs.json +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -10,7 +10,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Chcete nastavit televizi Samsung {model} ? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed. Ru\u010dn\u00ed konfigurace pro tuto televizi budou p\u0159eps\u00e1ny.", + "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index 4ab135993a1..279010485b6 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -27,22 +27,22 @@ "step": { "entity_config": { "data": { - "reverse": "Bedekking is omgekeerd" + "reverse": "Rolluik is omgekeerd" }, "description": "Configureer opties voor `{entity_id}`", "title": "Entiteit configureren" }, "init": { "data": { - "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde bedekkingen", + "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde rolluiken", "entity_id": "Configureer een specifieke entiteit.", - "target_id": "Configureer opties voor een bedekking." + "target_id": "Configureer opties voor een rolluik." }, "title": "Configureer MyLink-opties" }, "target_config": { "data": { - "reverse": "Bedekking is omgekeerd" + "reverse": "Rolluik is omgekeerd" }, "description": "Configureer opties voor ' {target_name} '", "title": "Configureer MyLink Cover" From ba6a0b57935677d129d8c5d87d6c9216114bce4d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Jun 2021 00:07:47 -0400 Subject: [PATCH 152/750] Fix no value error for heatit climate entities (#51392) --- .../zwave_js/discovery_data_template.py | 2 +- tests/components/zwave_js/conftest.py | 18 + tests/components/zwave_js/test_climate.py | 10 + .../climate_heatit_z_trm3_no_value_state.json | 1250 +++++++++++++++++ 4 files changed, 1279 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 4a2a8d2da94..7962b6b1c05 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -100,7 +100,7 @@ class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): lookup_table: dict[str | int, ZwaveValue | None] = resolved_data["lookup_table"] dependent_value: ZwaveValue | None = resolved_data["dependent_value"] - if dependent_value: + if dependent_value and dependent_value.value is not None: lookup_key = dependent_value.metadata.states[ str(dependent_value.value) ].split("-")[0] diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 12db8bafb77..caddbb050a5 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -261,6 +261,14 @@ def climate_heatit_z_trm2fx_state_fixture(): return json.loads(load_fixture("zwave_js/climate_heatit_z_trm2fx_state.json")) +@pytest.fixture(name="climate_heatit_z_trm3_no_value_state", scope="session") +def climate_heatit_z_trm3_no_value_state_fixture(): + """Load the climate HEATIT Z-TRM3 thermostat node w/no value state fixture data.""" + return json.loads( + load_fixture("zwave_js/climate_heatit_z_trm3_no_value_state.json") + ) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -517,6 +525,16 @@ def climate_eurotronic_spirit_z_fixture(client, climate_eurotronic_spirit_z_stat return node +@pytest.fixture(name="climate_heatit_z_trm3_no_value") +def climate_heatit_z_trm3_no_value_fixture( + client, climate_heatit_z_trm3_no_value_state +): + """Mock a climate radio HEATIT Z-TRM3 node.""" + node = Node(client, copy.deepcopy(climate_heatit_z_trm3_no_value_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_heatit_z_trm3") def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): """Mock a climate radio HEATIT Z-TRM3 node.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index a1b86b14ebc..f86052b3692 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -437,6 +437,16 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat client.async_send_command_no_wait.reset_mock() +async def test_thermostat_heatit_z_trm3_no_value( + hass, client, climate_heatit_z_trm3_no_value, integration +): + """Test a heatit Z-TRM3 entity that is missing a value.""" + # When the config parameter that specifies what sensor to use has no value, we fall + # back to the first temperature sensor found on the device + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + async def test_thermostat_heatit_z_trm3( hass, client, climate_heatit_z_trm3, integration ): diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json new file mode 100644 index 00000000000..50886b504a7 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_no_value_state.json @@ -0,0 +1,1250 @@ +{ + "nodeId": 74, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 411, + "productId": 515, + "productType": 3, + "firmwareVersion": "4.0", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x019b/z-trm3.json", + "manufacturer": "ThermoFloor", + "manufacturerId": 411, + "label": "Heatit Z-TRM3", + "description": "Floor thermostat", + "devices": [ + { + "productType": 3, + "productId": 515 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "overrideFloatEncoding": { + "size": 2 + }, + "addCCs": {} + }, + "isEmbedded": true + }, + "label": "Heatit Z-TRM3", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 74, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 74, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": null + }, + { + "nodeId": 74, + "index": 4, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": null + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" + }, + "value": 0.17 + }, + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2, 3, 4] + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Sensor mode", + "default": 1, + "min": 0, + "max": 4, + "states": { + "0": "F-mode, floor sensor mode", + "1": "A-mode, internal room sensor mode", + "2": "AF-mode, internal sensor and floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor type", + "default": 0, + "min": 0, + "max": 5, + "states": { + "0": "10K-NTC", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature control hysteresis (DIFF I)", + "default": 5, + "min": 3, + "max": 30, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor minimum temperature limit (FLo)", + "default": 50, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor maximum temperature (FHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "0.1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air minimum temperature limit (ALo)", + "default": 50, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air maximum temperature limit (AHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": ".1\u00b0C", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Room sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Room sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "External sensor calibration", + "default": 0, + "min": -60, + "max": 60, + "unit": ".1\u00b0C", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Selects which temperature is shown on the display.", + "label": "Temperature display", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Display setpoint temperature", + "1": "Display calculated temperature" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report hysteresis", + "default": 10, + "min": 1, + "max": 100, + "unit": "\u00b0C/10", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report interval", + "default": 90, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report delta value", + "default": 10, + "min": 0, + "max": 255, + "unit": "kWh/10", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 515 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.7" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["4.0", "3.2"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.81.6" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.7.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 97 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.0.10" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "min": 5, + "max": 35, + "unit": "\u00b0C" + }, + "value": 8 + }, + { + "endpoint": 1, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" + }, + "value": 2422.8 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + }, + "unit": "V" + }, + "value": 242.1 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 99 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 22.5 + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "UNKNOWN (0x00)", + "propertyName": "UNKNOWN (0x00)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "UNKNOWN (0x00)", + "ccSpecific": { + "sensorType": 0, + "scale": 0 + } + }, + "value": 23 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "UNKNOWN (0x00)", + "propertyName": "UNKNOWN (0x00)", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "UNKNOWN (0x00)", + "ccSpecific": { + "sensorType": 0, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Time", + "propertyName": "Time", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time", + "ccSpecific": { + "sensorType": 33, + "scale": 0 + }, + "unit": "s" + }, + "value": 3.2 + }, + { + "endpoint": 4, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 15.3 + } + ], + "neighbors": [1, 24, 25, 87, 88], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete" +} From 2c9e6bd9275030c939ec1a68f53641bc8fa14e56 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Jun 2021 23:10:27 -0500 Subject: [PATCH 153/750] Handle Sonos connection issues better when polling (#51376) --- homeassistant/components/sonos/entity.py | 6 +++++- homeassistant/components/sonos/media_player.py | 17 +++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 8c47c69b2d7..7d4e168c960 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -5,6 +5,7 @@ import datetime import logging from pysonos.core import SoCo +from pysonos.exceptions import SoCoException import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import ( @@ -70,7 +71,10 @@ class SonosEntity(Entity): self.speaker.subscription_address, ) self.speaker.is_first_poll = False - await self.async_update() # pylint: disable=no-member + try: + await self.async_update() # pylint: disable=no-member + except (OSError, SoCoException) as ex: + _LOGGER.debug("Error connecting to %s: %s", self.entity_id, ex) @property def soco(self) -> SoCo: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 133fe9efe8f..6d26363f83b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -13,7 +13,7 @@ from pysonos.core import ( PLAY_MODE_BY_MEANING, PLAY_MODES, ) -from pysonos.exceptions import SoCoException, SoCoUPnPException +from pysonos.exceptions import SoCoUPnPException from pysonos.plugins.sharelink import ShareLinkPlugin import voluptuous as vol @@ -294,18 +294,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_IDLE async def async_update(self) -> None: - """Retrieve latest state.""" + """Retrieve latest state by polling.""" await self.hass.async_add_executor_job(self._update) def _update(self) -> None: - """Retrieve latest state.""" - try: - self.speaker.update_groups() - self.speaker.update_volume() - if self.speaker.is_coordinator: - self.speaker.update_media() - except SoCoException: - pass + """Retrieve latest state by polling.""" + self.speaker.update_groups() + self.speaker.update_volume() + if self.speaker.is_coordinator: + self.speaker.update_media() @property def volume_level(self) -> float | None: From 55f158cf78c525ff9380a70a85229333f1819771 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 3 Jun 2021 12:31:39 +0800 Subject: [PATCH 154/750] Fix HLS idle timer in stream (#51372) --- homeassistant/components/stream/core.py | 9 ++++----- homeassistant/components/stream/hls.py | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index fcbc59ecdf3..f3d30fa6e1b 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -84,7 +84,7 @@ class StreamOutput: ) -> None: """Initialize a stream output.""" self._hass = hass - self._idle_timer = idle_timer + self.idle_timer = idle_timer self._event = asyncio.Event() self._segments: deque[Segment] = deque(maxlen=deque_maxlen) @@ -96,7 +96,7 @@ class StreamOutput: @property def idle(self) -> bool: """Return True if the output is idle.""" - return self._idle_timer.idle + return self.idle_timer.idle @property def last_sequence(self) -> int: @@ -139,7 +139,6 @@ class StreamOutput: async def recv(self) -> bool: """Wait for and retrieve the latest segment.""" - self._idle_timer.awake() await self._event.wait() return self.last_segment is not None @@ -151,7 +150,7 @@ class StreamOutput: def _async_put(self, segment: Segment) -> None: """Store output from event loop.""" # Start idle timeout when we start receiving data - self._idle_timer.start() + self.idle_timer.start() self._segments.append(segment) self._event.set() self._event.clear() @@ -159,7 +158,7 @@ class StreamOutput: def cleanup(self): """Handle cleanup.""" self._event.set() - self._idle_timer.clear() + self.idle_timer.clear() self._segments = deque(maxlen=self._segments.maxlen) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index be3598edb36..1d2921df192 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -165,6 +165,7 @@ class HlsSegmentView(StreamView): async def handle(self, request, stream, sequence): """Return fmp4 segment.""" track = stream.add_provider(HLS_PROVIDER) + track.idle_timer.awake() if not (segment := track.get_segment(int(sequence))): return web.HTTPNotFound() headers = {"Content-Type": "video/iso.segment"} From e8762bdea1b92fe091000c5be68e1056c8a08242 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 3 Jun 2021 08:39:44 +0200 Subject: [PATCH 155/750] Add binary sensor platform to SIA integration (#51206) * add support for binary_sensor * added default enabled for binary sensors * fixed coverage and a import deleted * disable pylint for line * Apply suggestions from code review * split binary sensor and used more attr fields Co-authored-by: Erik Montnemery --- .coveragerc | 2 + .../components/sia/alarm_control_panel.py | 165 +++--------------- homeassistant/components/sia/binary_sensor.py | 163 +++++++++++++++++ homeassistant/components/sia/const.py | 7 +- .../components/sia/sia_entity_base.py | 131 ++++++++++++++ 5 files changed, 323 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/sia/binary_sensor.py create mode 100644 homeassistant/components/sia/sia_entity_base.py diff --git a/.coveragerc b/.coveragerc index f788c917e82..c704566c8d9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -920,9 +920,11 @@ omit = homeassistant/components/slack/notify.py homeassistant/components/sia/__init__.py homeassistant/components/sia/alarm_control_panel.py + homeassistant/components/sia/binary_sensor.py homeassistant/components/sia/const.py homeassistant/components/sia/hub.py homeassistant/components/sia/utils.py + homeassistant/components/sia/sia_entity_base.py homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/__init__.py diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index fe5b95b639e..19ad4f4472e 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -9,7 +9,6 @@ from pysiaalarm import SIAEvent from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_PORT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -17,25 +16,12 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.core import HomeAssistant, State from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType -from .const import ( - CONF_ACCOUNT, - CONF_ACCOUNTS, - CONF_PING_INTERVAL, - CONF_ZONES, - DOMAIN, - SIA_EVENT, - SIA_NAME_FORMAT, - SIA_UNIQUE_ID_FORMAT_ALARM, -) -from .utils import get_attr_from_sia_event, get_unavailability_interval +from .const import CONF_ACCOUNT, CONF_ACCOUNTS, CONF_ZONES, SIA_UNIQUE_ID_FORMAT_ALARM +from .sia_entity_base import SIABaseEntity _LOGGER = logging.getLogger(__name__) @@ -86,7 +72,7 @@ async def async_setup_entry( ) -class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): +class SIAAlarmControlPanel(AlarmControlPanelEntity, SIABaseEntity): """Class for SIA Alarm Control Panels.""" def __init__( @@ -96,138 +82,31 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): zone: int, ) -> None: """Create SIAAlarmControlPanel object.""" - self._entry: ConfigEntry = entry - self._account_data: dict[str, Any] = account_data - self._zone: int = zone - - self._port: int = self._entry.data[CONF_PORT] - self._account: str = self._account_data[CONF_ACCOUNT] - self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] - - self._attr: dict[str, Any] = {} - - self._available: bool = True - self._state: StateType = None + super().__init__(entry, account_data, zone, DEVICE_CLASS_ALARM) + self._attr_state: StateType = None self._old_state: StateType = None - self._cancel_availability_cb: CALLBACK_TYPE | None = None - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass. - - Overridden from Entity. - - 1. register the dispatcher and add the callback to on_remove - 2. get previous state from storage - 3. if previous state: restore - 4. if previous state is unavailable: set _available to False and return - 5. if available: create availability cb - """ - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIA_EVENT.format(self._port, self._account), - self.async_handle_event, - ) - ) - last_state = await self.async_get_last_state() - if last_state is not None: - self._state = last_state.state - if self.state == STATE_UNAVAILABLE: - self._available = False - return - self._cancel_availability_cb = self.async_create_availability_cb() - - async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass. - - Overridden from Entity. - """ - if self._cancel_availability_cb: - self._cancel_availability_cb() - - async def async_handle_event(self, sia_event: SIAEvent) -> None: - """Listen to dispatcher events for this port and account and update state and attributes. - - If the port and account combo receives any message it means it is online and can therefore be set to available. - """ - _LOGGER.debug("Received event: %s", sia_event) - if int(sia_event.ri) == self._zone: - self._attr.update(get_attr_from_sia_event(sia_event)) - new_state = CODE_CONSEQUENCES.get(sia_event.code, None) - if new_state is not None: - if new_state == PREVIOUS_STATE: - new_state = self._old_state - self._state, self._old_state = new_state, self._state - self._available = True - self.async_write_ha_state() - self.async_reset_availability_cb() - - @callback - def async_reset_availability_cb(self) -> None: - """Reset availability cb by cancelling the current and creating a new one.""" - if self._cancel_availability_cb: - self._cancel_availability_cb() - self._cancel_availability_cb = self.async_create_availability_cb() - - @callback - def async_create_availability_cb(self) -> CALLBACK_TYPE: - """Create a availability cb and return the callback.""" - return async_call_later( - self.hass, - get_unavailability_interval(self._ping_interval), - self.async_set_unavailable, - ) - - @callback - def async_set_unavailable(self, _) -> None: - """Set unavailable.""" - self._available = False - self.async_write_ha_state() - - @property - def state(self) -> StateType: - """Get state.""" - return self._state - - @property - def name(self) -> str: - """Get Name.""" - return SIA_NAME_FORMAT.format( - self._port, self._account, self._zone, DEVICE_CLASS_ALARM - ) - - @property - def unique_id(self) -> str: - """Get unique_id.""" - return SIA_UNIQUE_ID_FORMAT_ALARM.format( + self._attr_unique_id = SIA_UNIQUE_ID_FORMAT_ALARM.format( self._entry.entry_id, self._account, self._zone ) - @property - def available(self) -> bool: - """Get availability.""" - return self._available + def update_state(self, sia_event: SIAEvent) -> None: + """Update the state of the alarm control panel.""" + new_state = CODE_CONSEQUENCES.get(sia_event.code, None) + if new_state is not None: + _LOGGER.debug("New state will be %s", new_state) + if new_state == PREVIOUS_STATE: + new_state = self._old_state + self._attr_state, self._old_state = new_state, self._attr_state - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device attributes.""" - return self._attr - - @property - def should_poll(self) -> bool: - """Return False if entity pushes its state to HA.""" - return False + def handle_last_state(self, last_state: State | None) -> None: + """Handle the last state.""" + if last_state is not None: + self._attr_state = last_state.state + if self.state == STATE_UNAVAILABLE: + self._attr_available = False @property def supported_features(self) -> int: - """Flag supported features.""" + """Return the list of supported features.""" return 0 - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "via_device": (DOMAIN, f"{self._port}_{self._account}"), - } diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py new file mode 100644 index 00000000000..f9cfca778e3 --- /dev/null +++ b/homeassistant/components/sia/binary_sensor.py @@ -0,0 +1,163 @@ +"""Module for SIA Binary Sensors.""" +from __future__ import annotations + +from collections.abc import Iterable +import logging +from typing import Any + +from pysiaalarm import SIAEvent + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ZONES, + SIA_HUB_ZONE, + SIA_UNIQUE_ID_FORMAT_BINARY, +) +from .sia_entity_base import SIABaseEntity + +_LOGGER = logging.getLogger(__name__) + + +POWER_CODE_CONSEQUENCES: dict[str, bool] = { + "AT": False, + "AR": True, +} + +SMOKE_CODE_CONSEQUENCES: dict[str, bool] = { + "GA": True, + "GH": False, + "FA": True, + "FH": False, + "KA": True, + "KH": False, +} + +MOISTURE_CODE_CONSEQUENCES: dict[str, bool] = { + "WA": True, + "WH": False, +} + + +def generate_binary_sensors(entry) -> Iterable[SIABinarySensorBase]: + """Generate binary sensors. + + For each Account there is one power sensor with zone == 0. + For each Zone in each Account there is one smoke and one moisture sensor. + """ + for account in entry.data[CONF_ACCOUNTS]: + yield SIABinarySensorPower(entry, account) + zones = entry.options[CONF_ACCOUNTS][account[CONF_ACCOUNT]][CONF_ZONES] + for zone in range(1, zones + 1): + yield SIABinarySensorSmoke(entry, account, zone) + yield SIABinarySensorMoisture(entry, account, zone) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up SIA binary sensors from a config entry.""" + async_add_entities(generate_binary_sensors(entry)) + + +class SIABinarySensorBase(BinarySensorEntity, SIABaseEntity): + """Class for SIA Binary Sensors.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + zone: int, + device_class: str, + ) -> None: + """Initialize a base binary sensor.""" + super().__init__(entry, account_data, zone, device_class) + + self._attr_unique_id = SIA_UNIQUE_ID_FORMAT_BINARY.format( + self._entry.entry_id, self._account, self._zone, self._attr_device_class + ) + + def handle_last_state(self, last_state: State | None) -> None: + """Handle the last state.""" + if last_state is not None and last_state.state is not None: + if last_state.state == STATE_ON: + self._attr_is_on = True + elif last_state.state == STATE_OFF: + self._attr_is_on = False + elif last_state.state == STATE_UNAVAILABLE: + self._attr_available = False + + +class SIABinarySensorMoisture(SIABinarySensorBase): + """Class for Moisture Binary Sensors.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + zone: int, + ) -> None: + """Initialize a Moisture binary sensor.""" + super().__init__(entry, account_data, zone, DEVICE_CLASS_MOISTURE) + self._attr_entity_registry_enabled_default = False + + def update_state(self, sia_event: SIAEvent) -> None: + """Update the state of the binary sensor.""" + new_state = MOISTURE_CODE_CONSEQUENCES.get(sia_event.code, None) + if new_state is not None: + _LOGGER.debug("New state will be %s", new_state) + self._attr_is_on = new_state + + +class SIABinarySensorSmoke(SIABinarySensorBase): + """Class for Smoke Binary Sensors.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + zone: int, + ) -> None: + """Initialize a Smoke binary sensor.""" + super().__init__(entry, account_data, zone, DEVICE_CLASS_SMOKE) + self._attr_entity_registry_enabled_default = False + + def update_state(self, sia_event: SIAEvent) -> None: + """Update the state of the binary sensor.""" + new_state = SMOKE_CODE_CONSEQUENCES.get(sia_event.code, None) + if new_state is not None: + _LOGGER.debug("New state will be %s", new_state) + self._attr_is_on = new_state + + +class SIABinarySensorPower(SIABinarySensorBase): + """Class for Power Binary Sensors.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + ) -> None: + """Initialize a Power binary sensor.""" + super().__init__(entry, account_data, SIA_HUB_ZONE, DEVICE_CLASS_POWER) + self._attr_entity_registry_enabled_default = True + + def update_state(self, sia_event: SIAEvent) -> None: + """Update the state of the binary sensor.""" + new_state = POWER_CODE_CONSEQUENCES.get(sia_event.code, None) + if new_state is not None: + _LOGGER.debug("New state will be %s", new_state) + self._attr_is_on = new_state diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index 916cdb9621c..711c070b1ee 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -2,13 +2,14 @@ from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] +PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN] DOMAIN = "sia" ATTR_CODE = "last_code" -ATTR_ZONE = "zone" +ATTR_ZONE = "last_zone" ATTR_MESSAGE = "last_message" ATTR_ID = "last_id" ATTR_TIMESTAMP = "last_timestamp" @@ -24,5 +25,7 @@ CONF_ZONES = "zones" SIA_NAME_FORMAT = "{} - {} - zone {} - {}" SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" +SIA_UNIQUE_ID_FORMAT_BINARY = "{}_{}_{}_{}" +SIA_HUB_ZONE = 0 SIA_EVENT = "sia_event_{}_{}" diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py new file mode 100644 index 00000000000..9d81b749afe --- /dev/null +++ b/homeassistant/components/sia/sia_entity_base.py @@ -0,0 +1,131 @@ +"""Module for SIA Base Entity.""" +from __future__ import annotations + +from abc import abstractmethod +import logging +from typing import Any + +from pysiaalarm import SIAEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT +from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import CONF_ACCOUNT, CONF_PING_INTERVAL, DOMAIN, SIA_EVENT, SIA_NAME_FORMAT +from .utils import get_attr_from_sia_event, get_unavailability_interval + +_LOGGER = logging.getLogger(__name__) + + +class SIABaseEntity(RestoreEntity): + """Base class for SIA entities.""" + + def __init__( + self, + entry: ConfigEntry, + account_data: dict[str, Any], + zone: int, + device_class: str, + ) -> None: + """Create SIABaseEntity object.""" + self._entry: ConfigEntry = entry + self._account_data: dict[str, Any] = account_data + self._zone: int = zone + self._attr_device_class: str = device_class + + self._port: int = self._entry.data[CONF_PORT] + self._account: str = self._account_data[CONF_ACCOUNT] + self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] + + self._cancel_availability_cb: CALLBACK_TYPE | None = None + + self._attr_extra_state_attributes: dict[str, Any] = {} + self._attr_should_poll = False + self._attr_name = SIA_NAME_FORMAT.format( + self._port, self._account, self._zone, self._attr_device_class + ) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass. + + Overridden from Entity. + + 1. register the dispatcher and add the callback to on_remove + 2. get previous state from storage and pass to entity specific function + 3. if available: create availability cb + """ + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIA_EVENT.format(self._port, self._account), + self.async_handle_event, + ) + ) + self.handle_last_state(await self.async_get_last_state()) + if self._attr_available: + self.async_create_availability_cb() + + @abstractmethod + def handle_last_state(self, last_state: State | None) -> None: + """Handle the last state.""" + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass. + + Overridden from Entity. + """ + if self._cancel_availability_cb: + self._cancel_availability_cb() + + async def async_handle_event(self, sia_event: SIAEvent) -> None: + """Listen to dispatcher events for this port and account and update state and attributes. + + If the port and account combo receives any message it means it is online and can therefore be set to available. + """ + _LOGGER.debug("Received event: %s", sia_event) + if int(sia_event.ri) == self._zone: + self._attr_extra_state_attributes.update(get_attr_from_sia_event(sia_event)) + self.update_state(sia_event) + self.async_reset_availability_cb() + self.async_write_ha_state() + + @abstractmethod + def update_state(self, sia_event: SIAEvent) -> None: + """Do the entity specific state updates.""" + + @callback + def async_reset_availability_cb(self) -> None: + """Reset availability cb by cancelling the current and creating a new one.""" + self._attr_available = True + if self._cancel_availability_cb: + self._cancel_availability_cb() + self.async_create_availability_cb() + + def async_create_availability_cb(self) -> None: + """Create a availability cb and return the callback.""" + self._cancel_availability_cb = async_call_later( + self.hass, + get_unavailability_interval(self._ping_interval), + self.async_set_unavailable, + ) + + @callback + def async_set_unavailable(self, _) -> None: + """Set unavailable.""" + self._attr_available = False + self.async_write_ha_state() + + @property + def device_info(self) -> DeviceInfo: + """Return the device_info.""" + assert self._attr_name is not None + assert self.unique_id is not None + return { + "name": self._attr_name, + "identifiers": {(DOMAIN, self.unique_id)}, + "via_device": (DOMAIN, f"{self._port}_{self._account}"), + } From 1062acfe9b7ca679d077b8952cc6143379dfe475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 3 Jun 2021 11:59:22 +0200 Subject: [PATCH 156/750] Fix Tibber Pulse device name and sensor update (#51402) --- homeassistant/components/tibber/sensor.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index f2ff23dfe5d..b60d88e9814 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -186,6 +186,7 @@ class TibberSensor(SensorEntity): """Initialize the sensor.""" self._tibber_home = tibber_home self._home_name = tibber_home.info["viewer"]["home"]["appNickname"] + self._device_name = None if self._home_name is None: self._home_name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" @@ -202,7 +203,7 @@ class TibberSensor(SensorEntity): """Return the device_info of the device.""" device_info = { "identifiers": {(TIBBER_DOMAIN, self.device_id)}, - "name": self.name, + "name": self._device_name, "manufacturer": MANUFACTURER, } if self._model is not None: @@ -237,6 +238,8 @@ class TibberSensorElPrice(TibberSensor): self._attr_unique_id = f"{self._tibber_home.home_id}" self._model = "Price Sensor" + self._device_name = self._attr_name + async def async_update(self): """Get the latest data and updates the states.""" now = dt_util.now() @@ -295,6 +298,7 @@ class TibberSensorRT(TibberSensor): super().__init__(tibber_home) self._sensor_name = sensor_name self._model = "Tibber Pulse" + self._device_name = f"{self._model} {self._home_name}" self._attr_device_class = device_class self._attr_name = f"{self._sensor_name} {self._home_name}" @@ -330,7 +334,7 @@ class TibberSensorRT(TibberSensor): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_UPDATE_ENTITY.format(self._sensor_name), + SIGNAL_UPDATE_ENTITY.format(self.unique_id), self._set_state, ) ) @@ -370,7 +374,7 @@ class TibberRtDataHandler: self._async_add_entities = async_add_entities self._tibber_home = tibber_home self.hass = hass - self._entities = set() + self._entities = {} async def async_callback(self, payload): """Handle received data.""" @@ -393,7 +397,7 @@ class TibberRtDataHandler: if sensor_type in self._entities: async_dispatcher_send( self.hass, - SIGNAL_UPDATE_ENTITY.format(RT_SENSOR_MAP[sensor_type][0]), + SIGNAL_UPDATE_ENTITY.format(self._entities[sensor_type]), state, timestamp, ) @@ -412,6 +416,6 @@ class TibberRtDataHandler: state_class, ) new_entities.append(entity) - self._entities.add(sensor_type) + self._entities[sensor_type] = entity.unique_id if new_entities: self._async_add_entities(new_entities) From 470514cb086b6d0483454491fcf585ba986ceba0 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 3 Jun 2021 12:40:00 +0200 Subject: [PATCH 157/750] Fix shopping list "complete all" service name (#51406) --- homeassistant/components/shopping_list/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index 9f5437701ed..7bf209550d7 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -34,7 +34,7 @@ incomplete_item: text: complete_all: - name: Complete call + name: Complete all description: Marks all items as completed in the shopping list. It does not remove the items. incomplete_all: From a6902ffd8a43bb93b7567be2b193452c38d64acb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Jun 2021 14:21:52 +0200 Subject: [PATCH 158/750] Remove is_standby from SwitchEntity (#51400) --- homeassistant/components/hdmi_cec/switch.py | 7 +------ homeassistant/components/switch/__init__.py | 6 ------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index ea0cac76a99..5de38675fca 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY +from homeassistant.const import STATE_OFF, STATE_ON from . import ATTR_NEW, CecEntity @@ -56,11 +56,6 @@ class CecSwitchEntity(CecEntity, SwitchEntity): """Return True if entity is on.""" return self._state == STATE_ON - @property - def is_standby(self): - """Return true if device is in standby.""" - return self._state == STATE_OFF or self._state == STATE_STANDBY - @property def state(self) -> str: """Return the cached state of device.""" diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index f468f6f6bd3..f9e9542701b 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -88,7 +88,6 @@ class SwitchEntity(ToggleEntity): """Base class for switch entities.""" _attr_current_power_w: float | None = None - _attr_is_standby: bool | None = None _attr_today_energy_kwh: float | None = None @property @@ -101,11 +100,6 @@ class SwitchEntity(ToggleEntity): """Return the today total energy usage in kWh.""" return self._attr_today_energy_kwh - @property - def is_standby(self) -> bool | None: - """Return true if device is in standby.""" - return self._attr_is_standby - @final @property def state_attributes(self) -> dict[str, Any] | None: From ffe8b3e49bdd612c9cf7eefa098f83808fd36c6a Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Thu, 3 Jun 2021 14:39:42 +0200 Subject: [PATCH 159/750] Add bosch shc platforms for sensor devices (#50720) --- .coveragerc | 3 +- .../components/bosch_shc/__init__.py | 4 +- .../components/bosch_shc/binary_sensor.py | 48 ++- homeassistant/components/bosch_shc/sensor.py | 406 ++++++++++++++++++ 4 files changed, 456 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bosch_shc/sensor.py diff --git a/.coveragerc b/.coveragerc index c704566c8d9..bb84fc73823 100644 --- a/.coveragerc +++ b/.coveragerc @@ -115,9 +115,10 @@ omit = homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/bosch_shc/__init__.py - homeassistant/components/bosch_shc/const.py homeassistant/components/bosch_shc/binary_sensor.py + homeassistant/components/bosch_shc/const.py homeassistant/components/bosch_shc/entity.py + homeassistant/components/bosch_shc/sensor.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index a315405365c..f68a2b68467 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -19,9 +19,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = [ - "binary_sensor", -] +PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index ef2d35097e1..d2c2df838c5 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -1,7 +1,8 @@ """Platform for binarysensor integration.""" -from boschshcpy import SHCSession, SHCShutterContact +from boschshcpy import SHCBatteryDevice, SHCSession, SHCShutterContact from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_DOOR, DEVICE_CLASS_WINDOW, BinarySensorEntity, @@ -25,6 +26,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) + for binary_sensor in ( + session.device_helper.motion_detectors + + session.device_helper.shutter_contacts + + session.device_helper.smoke_detectors + + session.device_helper.thermostats + + session.device_helper.twinguards + + session.device_helper.universal_switches + + session.device_helper.wallthermostats + + session.device_helper.water_leakage_detectors + ): + if binary_sensor.supports_batterylevel: + entities.append( + BatterySensor( + device=binary_sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + if entities: async_add_entities(entities) @@ -47,3 +67,29 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity): "GENERIC": DEVICE_CLASS_WINDOW, } return switcher.get(self._device.device_class, DEVICE_CLASS_WINDOW) + + +class BatterySensor(SHCEntity, BinarySensorEntity): + """Representation of a SHC battery reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_battery" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Battery" + + @property + def is_on(self): + """Return the state of the sensor.""" + return ( + self._device.batterylevel != SHCBatteryDevice.BatteryLevelService.State.OK + ) + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS_BATTERY diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py new file mode 100644 index 00000000000..9f3cf2d5bc3 --- /dev/null +++ b/homeassistant/components/bosch_shc/sensor.py @@ -0,0 +1,406 @@ +"""Platform for sensor integration.""" +from boschshcpy import SHCSession + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, +) + +from .const import DATA_SESSION, DOMAIN +from .entity import SHCEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the SHC sensor platform.""" + entities = [] + session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] + + for sensor in session.device_helper.thermostats: + entities.append( + TemperatureSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + ValveTappetSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for sensor in session.device_helper.wallthermostats: + entities.append( + TemperatureSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + HumiditySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for sensor in session.device_helper.twinguards: + entities.append( + TemperatureSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + HumiditySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + PuritySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + AirQualitySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + TemperatureRatingSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + HumidityRatingSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + PurityRatingSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for sensor in session.device_helper.smart_plugs: + entities.append( + PowerSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + EnergySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for sensor in session.device_helper.smart_plugs_compact: + entities.append( + PowerSensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + entities.append( + EnergySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + if entities: + async_add_entities(entities) + + +class TemperatureSensor(SHCEntity, SensorEntity): + """Representation of a SHC temperature reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_temperature" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Temperature" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.temperature + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return TEMP_CELSIUS + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_TEMPERATURE + + +class HumiditySensor(SHCEntity, SensorEntity): + """Representation of a SHC humidity reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_humidity" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Humidity" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.humidity + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return PERCENTAGE + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_HUMIDITY + + +class PuritySensor(SHCEntity, SensorEntity): + """Representation of a SHC purity reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_purity" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Purity" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.purity + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return CONCENTRATION_PARTS_PER_MILLION + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:molecule-co2" + + +class AirQualitySensor(SHCEntity, SensorEntity): + """Representation of a SHC airquality reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_airquality" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Air Quality" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.combined_rating.name + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + "rating_description": self._device.description, + } + + +class TemperatureRatingSensor(SHCEntity, SensorEntity): + """Representation of a SHC temperature rating sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_temperature_rating" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Temperature Rating" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.temperature_rating.name + + +class HumidityRatingSensor(SHCEntity, SensorEntity): + """Representation of a SHC humidity rating sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_humidity_rating" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Humidity Rating" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.humidity_rating.name + + +class PurityRatingSensor(SHCEntity, SensorEntity): + """Representation of a SHC purity rating sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_purity_rating" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Purity Rating" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.purity_rating.name + + +class PowerSensor(SHCEntity, SensorEntity): + """Representation of a SHC power reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_power" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Power" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.powerconsumption + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_POWER + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return POWER_WATT + + +class EnergySensor(SHCEntity, SensorEntity): + """Representation of a SHC energy reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_energy" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Energy" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.energyconsumption / 1000.0 + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_ENERGY + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return ENERGY_KILO_WATT_HOUR + + +class ValveTappetSensor(SHCEntity, SensorEntity): + """Representation of a SHC valve tappet reporting sensor.""" + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._device.serial}_valvetappet" + + @property + def name(self): + """Return the name of this sensor.""" + return f"{self._device.name} Valvetappet" + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.position + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:gauge" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the sensor.""" + return PERCENTAGE + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + "valve_tappet_state": self._device.valvestate.name, + } From 53ae340900d67c73ee7e0d9b83ea4bc245a0eb72 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Thu, 3 Jun 2021 18:34:28 +0200 Subject: [PATCH 160/750] Bumped to boschshcpy==0.2.19 (#51416) * Bumped to boschshcpy==0.2.19 * update requirements --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 7922450ccde..d4a8498fba3 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.17"], + "requirements": ["boschshcpy==0.2.19"], "zeroconf": [ {"type": "_http._tcp.local.", "name": "bosch shc*"} ], diff --git a/requirements_all.txt b/requirements_all.txt index 447d4e238ce..c7522635360 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ blockchain==1.4.4 bond-api==0.1.12 # homeassistant.components.bosch_shc -boschshcpy==0.2.17 +boschshcpy==0.2.19 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 14b103584be..c101e11acd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -220,7 +220,7 @@ blinkpy==0.17.0 bond-api==0.1.12 # homeassistant.components.bosch_shc -boschshcpy==0.2.17 +boschshcpy==0.2.19 # homeassistant.components.braviatv bravia-tv==1.0.11 From c1111afef82c799584005e1b9f575a4ab03e9a04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Jun 2021 08:26:37 -1000 Subject: [PATCH 161/750] Allow registering a callback to ssdp that matches any key value (#51382) --- homeassistant/components/ssdp/__init__.py | 40 +++++++++++++---------- tests/components/ssdp/test_init.py | 30 ++++++++++++++++- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 5fcd58e14f7..3cd2cb87f70 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -13,7 +13,11 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + MATCH_ALL, +) from homeassistant.core import CoreState, HomeAssistant, callback as core_callback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -128,6 +132,19 @@ def _async_process_callbacks( _LOGGER.exception("Failed to callback info: %s", discovery_info) +@core_callback +def _async_headers_match( + headers: Mapping[str, str], match_dict: dict[str, str] +) -> bool: + for header, val in match_dict.items(): + if val == MATCH_ALL: + if header not in headers: + return False + elif headers.get(header) != val: + return False + return True + + class Scanner: """Class to manage SSDP scanning.""" @@ -157,7 +174,10 @@ class Scanner: # before the callback was registered are fired if self.hass.state != CoreState.running: for headers in self.cache.values(): - self._async_callback_if_match(callback, headers, match_dict) + if _async_headers_match(headers, match_dict): + _async_process_callbacks( + [callback], self._async_headers_to_discovery_info(headers) + ) callback_entry = (callback, match_dict) self._callbacks.append(callback_entry) @@ -168,20 +188,6 @@ class Scanner: return _async_remove_callback - @core_callback - def _async_callback_if_match( - self, - callback: Callable[[dict], None], - headers: Mapping[str, str], - match_dict: dict[str, str], - ) -> None: - """Fire a callback if info matches the match dict.""" - if not all(headers.get(k) == v for (k, v) in match_dict.items()): - return - _async_process_callbacks( - [callback], self._async_headers_to_discovery_info(headers) - ) - @core_callback def async_stop(self, *_: Any) -> None: """Stop the scanner.""" @@ -250,7 +256,7 @@ class Scanner: return [ callback for callback, match_dict in self._callbacks - if all(headers.get(k) == v for (k, v) in match_dict.items()) + if _async_headers_match(headers, match_dict) ] @core_callback diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index f5418bd227e..6b41544a384 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -11,7 +11,11 @@ import pytest from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + MATCH_ALL, +) from homeassistant.core import CoreState, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -356,9 +360,12 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): "location": "http://1.1.1.1", "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", "server": "mock-server", + "x-rincon-bootseq": "55", "ext": "", } not_matching_intergration_callbacks = [] + intergration_match_all_callbacks = [] + intergration_match_all_not_present_callbacks = [] intergration_callbacks = [] intergration_callbacks_from_cache = [] match_any_callbacks = [] @@ -371,6 +378,14 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): def _async_intergration_callbacks(info): intergration_callbacks.append(info) + @callback + def _async_intergration_match_all_callbacks(info): + intergration_match_all_callbacks.append(info) + + @callback + def _async_intergration_match_all_not_present_callbacks(info): + intergration_match_all_not_present_callbacks.append(info) + @callback def _async_intergration_callbacks_from_cache(info): intergration_callbacks_from_cache.append(info) @@ -410,6 +425,16 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): _async_intergration_callbacks, {"st": "mock-st"}, ) + ssdp.async_register_callback( + hass, + _async_intergration_match_all_callbacks, + {"x-rincon-bootseq": MATCH_ALL}, + ) + ssdp.async_register_callback( + hass, + _async_intergration_match_all_not_present_callbacks, + {"x-not-there": MATCH_ALL}, + ) ssdp.async_register_callback( hass, _async_not_matching_intergration_callbacks, @@ -436,6 +461,8 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): assert len(intergration_callbacks) == 3 assert len(intergration_callbacks_from_cache) == 3 + assert len(intergration_match_all_callbacks) == 3 + assert len(intergration_match_all_not_present_callbacks) == 0 assert len(match_any_callbacks) == 3 assert len(not_matching_intergration_callbacks) == 0 assert intergration_callbacks[0] == { @@ -446,6 +473,7 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): ssdp.ATTR_SSDP_ST: "mock-st", ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + "x-rincon-bootseq": "55", } assert "Failed to callback info" in caplog.text From b3327e16569d858c84b72eb20f04c42b81dc0808 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:11:45 -0400 Subject: [PATCH 162/750] Bump zwave-js-server-python to 0.26.1 (#51425) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 5ce65fcbb35..fd342b8d498 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.26.0"], + "requirements": ["zwave-js-server-python==0.26.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c7522635360..981e5f4a3cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2441,4 +2441,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.0 +zwave-js-server-python==0.26.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c101e11acd8..61c77f5a998 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1323,4 +1323,4 @@ zigpy-znp==0.5.1 zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.0 +zwave-js-server-python==0.26.1 From 836ce442f7d682dff145a9488290259c770ba071 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 3 Jun 2021 21:51:09 +0100 Subject: [PATCH 163/750] Bump aiohomekit to 0.2.67 (fixes #51391) (#51418) --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index c18ee9e574f..7ff32e402fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.66"], + "requirements": ["aiohomekit==0.2.67"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 981e5f4a3cf..d4def1cfb30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.66 +aiohomekit==0.2.67 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61c77f5a998..d95a1c029fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.66 +aiohomekit==0.2.67 # homeassistant.components.emulated_hue # homeassistant.components.http From f3b2624be30b650a8a385e37f04ad7151deced3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Jun 2021 14:12:39 -0700 Subject: [PATCH 164/750] Pin jinja (#51434) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c83f4625894..b4cc9202a7b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ hass-nabucasa==0.43.0 home-assistant-frontend==20210601.1 httpx==0.18.0 ifaddr==0.1.7 -jinja2>=3.0.1 +jinja2==3.0.1 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 diff --git a/requirements.txt b/requirements.txt index 7d9b7739669..ad9c2717e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.18.0 -jinja2>=3.0.1 +jinja2==3.0.1 PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 diff --git a/setup.py b/setup.py index 0178b201372..758f4f3813d 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.18.0", - "jinja2>=3.0.1", + "jinja2==3.0.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.3.2", From 65f23c45a80e47fb63085b1b84e92dae64ef910f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 4 Jun 2021 00:41:59 +0200 Subject: [PATCH 165/750] Update frontend to 20210603.0 (#51442) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 42f29f36976..8f5f5f091d9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210601.1" + "home-assistant-frontend==20210603.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4cc9202a7b..8705b8551f8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index d4def1cfb30..a331922524c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d95a1c029fe..4367a1b79b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210601.1 +home-assistant-frontend==20210603.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a232f2ce7d6fff23b9d8b782bf6ae1466b189cc5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 4 Jun 2021 00:42:59 +0200 Subject: [PATCH 166/750] Fix last activity consideration for AVM Fritz!Tools device tracker (#51375) --- homeassistant/components/fritz/common.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index ec7e402f760..84288fe7fb3 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -217,21 +217,22 @@ class FritzDevice: """Update device info.""" utc_point_in_time = dt_util.utcnow() - if not self._name: - self._name = dev_info.name or self._mac.replace(":", "_") - - if not dev_home and self._last_activity: - self._connected = ( + if self._last_activity: + consider_home_evaluated = ( utc_point_in_time - self._last_activity ).total_seconds() < consider_home else: - self._connected = dev_home + consider_home_evaluated = dev_home - if self._connected: - self._ip_address = dev_info.ip_address + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + + self._connected = dev_home or consider_home_evaluated + + if dev_home: self._last_activity = utc_point_in_time - else: - self._ip_address = None + + self._ip_address = dev_info.ip_address if self._connected else None @property def is_connected(self): From bf3a561149dbe4d8e88237826e78f0ad82688346 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Jun 2021 17:16:32 -0700 Subject: [PATCH 167/750] Bump aiohue to 2.5.1 (#51447) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b61635cb408..3c8078364ab 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.5.0"], + "requirements": ["aiohue==2.5.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index a331922524c..2ef6bd7f856 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.67 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.0 +aiohue==2.5.1 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4367a1b79b2..725bb982cc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.67 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.5.0 +aiohue==2.5.1 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 07d6186feab534d05ef88ad7c7b68e98fd751584 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 4 Jun 2021 02:43:15 +0200 Subject: [PATCH 168/750] Small fixes in SIA (#51401) * fixes from comment in #51206 * reverted name to async_ --- homeassistant/components/sia/alarm_control_panel.py | 2 +- homeassistant/components/sia/binary_sensor.py | 2 +- homeassistant/components/sia/sia_entity_base.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 19ad4f4472e..5a6a4f6f55c 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -72,7 +72,7 @@ async def async_setup_entry( ) -class SIAAlarmControlPanel(AlarmControlPanelEntity, SIABaseEntity): +class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity): """Class for SIA Alarm Control Panels.""" def __init__( diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index f9cfca778e3..eec4f9b2717 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -73,7 +73,7 @@ async def async_setup_entry( async_add_entities(generate_binary_sensors(entry)) -class SIABinarySensorBase(BinarySensorEntity, SIABaseEntity): +class SIABinarySensorBase(SIABaseEntity, BinarySensorEntity): """Class for SIA Binary Sensors.""" def __init__( diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 9d81b749afe..5169702e67b 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -81,7 +81,8 @@ class SIABaseEntity(RestoreEntity): if self._cancel_availability_cb: self._cancel_availability_cb() - async def async_handle_event(self, sia_event: SIAEvent) -> None: + @callback + def async_handle_event(self, sia_event: SIAEvent) -> None: """Listen to dispatcher events for this port and account and update state and attributes. If the port and account combo receives any message it means it is online and can therefore be set to available. From cf954881f68c9bdc9d15790d68f536f384f2a3c9 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 3 Jun 2021 23:24:38 -0700 Subject: [PATCH 169/750] Address Hyperion camera post-merge code review (#51457) --- homeassistant/components/hyperion/camera.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 733a91c1c58..1ef88969228 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -9,7 +9,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager import functools import logging -from typing import Any, Callable +from typing import Any from aiohttp import web from hyperion import client @@ -33,6 +33,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from . import ( @@ -56,8 +57,10 @@ IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64," async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable -) -> bool: + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up a Hyperion platform from config entry.""" entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = config_entry.unique_id @@ -94,7 +97,6 @@ async def async_setup_entry( ) listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) - return True # A note on Hyperion streaming semantics: @@ -232,7 +234,6 @@ class HyperionCamera(Camera): async def async_added_to_hass(self) -> None: """Register callbacks when entity added to hass.""" - assert self.hass self.async_on_remove( async_dispatcher_connect( self.hass, From 68f6506ff95997c53c4afdaaaab7c557f4b62c2a Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Thu, 3 Jun 2021 23:32:01 -0700 Subject: [PATCH 170/750] Update to iaqualink 0.3.90 (#51452) --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index b3aa257a9b2..26c6e0b4bfd 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.4"], + "requirements": ["iaqualink==0.3.90"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2ef6bd7f856..cbfa6e5bdf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hyperion-py==0.7.4 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.3.4 +iaqualink==0.3.90 # homeassistant.components.watson_tts ibm-watson==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 725bb982cc6..5b5f14492b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -457,7 +457,7 @@ huisbaasje-client==0.1.0 hyperion-py==0.7.4 # homeassistant.components.iaqualink -iaqualink==0.3.4 +iaqualink==0.3.90 # homeassistant.components.ping icmplib==3.0 From e5c70c87896686717d45f5fe7d06b3af064a93ef Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 4 Jun 2021 08:34:16 +0200 Subject: [PATCH 171/750] Update xknx to version 0.18.4 (#51459) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b1ed504f7ad..6b1d4d328ac 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.3"], + "requirements": ["xknx==0.18.4"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index cbfa6e5bdf5..732c0affeac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2374,7 +2374,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.3 +xknx==0.18.4 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b5f14492b1..3cae890e6ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1280,7 +1280,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.3 +xknx==0.18.4 # homeassistant.components.bluesound # homeassistant.components.rest From 5fc1822b43261ac4b5d477c001b3c29457c67614 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Jun 2021 20:54:45 -1000 Subject: [PATCH 172/750] Retry isy994 setup later if isy.initialize times out (#51453) Maybe fixes https://forum.universal-devices.com/topic/26633-home-assistant-isy-component/?do=findComment&comment=312147 --- homeassistant/components/isy994/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 27d81a671c8..99905e4d946 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,6 +1,7 @@ """Support the ISY-994 controllers.""" from __future__ import annotations +import asyncio from urllib.parse import urlparse from aiohttp import CookieJar @@ -171,8 +172,14 @@ async def async_setup_entry( ) try: - with async_timeout.timeout(30): + async with async_timeout.timeout(30): await isy.initialize() + except asyncio.TimeoutError as err: + _LOGGER.error( + "Timed out initializing the ISY; device may be busy, trying again later: %s", + err, + ) + raise ConfigEntryNotReady from err except ISYInvalidAuthError as err: _LOGGER.error( "Invalid credentials for the ISY, please adjust settings and try again: %s", From 12f2482c9bb062f347cd1e80a68e9d0d469f897a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 4 Jun 2021 16:26:44 +0100 Subject: [PATCH 173/750] Bump aiolyric to 1.0.7 (#51473) --- homeassistant/components/lyric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 6317c6c3357..fbcc9567c3a 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", "dependencies": ["http"], - "requirements": ["aiolyric==1.0.6"], + "requirements": ["aiolyric==1.0.7"], "codeowners": ["@timmo001"], "quality_scale": "silver", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 732c0affeac..0b4c75e8530 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.4 # homeassistant.components.lyric -aiolyric==1.0.6 +aiolyric==1.0.7 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cae890e6ca..4468e3509b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiokafka==0.6.0 aiolip==1.1.4 # homeassistant.components.lyric -aiolyric==1.0.6 +aiolyric==1.0.7 # homeassistant.components.notion aionotion==1.1.0 From 3d8804bbcfd968b23eaf858d9a8ebd004ecf1318 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Jun 2021 18:02:39 +0200 Subject: [PATCH 174/750] Improve logging for SamsungTV (#51477) --- homeassistant/components/samsungtv/bridge.py | 10 ++++++---- homeassistant/components/samsungtv/config_flow.py | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 84b518a4633..7e1c24c6d2f 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -184,7 +184,9 @@ class SamsungTVLegacyBridge(SamsungTVBridge): if self._remote is None: # We need to create a new instance to reconnect. try: - LOGGER.debug("Create SamsungRemote") + LOGGER.debug( + "Create SamsungTVLegacyBridge for %s (%s)", CONF_NAME, self.host + ) self._remote = Remote(self.config.copy()) # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket @@ -199,7 +201,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): def stop(self): """Stop Bridge.""" - LOGGER.debug("Stopping SamsungRemote") + LOGGER.debug("Stopping SamsungTVLegacyBridge") self.close_remote() @@ -272,7 +274,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # We need to create a new instance to reconnect. try: LOGGER.debug( - "Create SamsungTVWS for %s (%s)", VALUE_CONF_NAME, self.host + "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host ) self._remote = SamsungTVWS( host=self.host, @@ -293,5 +295,5 @@ class SamsungTVWSBridge(SamsungTVBridge): def stop(self): """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVWS") + LOGGER.debug("Stopping SamsungTVWSBridge") self.close_remote() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 46800e1653b..7c6dea56b96 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -224,6 +224,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" + LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) await self._async_set_unique_id_from_udn() await self._async_start_discovery_for_host( @@ -242,6 +243,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by dhcp discovery.""" + LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info[MAC_ADDRESS] await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) await self._async_set_device_unique_id() @@ -250,6 +252,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by zeroconf discovery.""" + LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) await self._async_set_device_unique_id() From e41e153220efc76499f9cebb603e204437039edf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Jun 2021 18:02:59 +0200 Subject: [PATCH 175/750] Upgrade elgato to 2.1.1 (#51483) --- homeassistant/components/elgato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index dbb83f18995..7a095cb5917 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==2.1.0"], + "requirements": ["elgato==2.1.1"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 0b4c75e8530..7ab48c49a43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -533,7 +533,7 @@ ebusdpy==0.0.16 ecoaliface==0.4.0 # homeassistant.components.elgato -elgato==2.1.0 +elgato==2.1.1 # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4468e3509b6..5d9b9cee4ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ dsmr_parser==0.29 dynalite_devices==0.1.46 # homeassistant.components.elgato -elgato==2.1.0 +elgato==2.1.1 # homeassistant.components.elkm1 elkm1-lib==0.8.10 From 5e067c26312f65e605d3ba0e7927d546d9f4312d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 4 Jun 2021 18:06:44 +0200 Subject: [PATCH 176/750] Allow unlimited scan_interval in modbus (#51471) Co-authored-by: Franck Nijhof --- homeassistant/components/modbus/const.py | 1 - homeassistant/components/modbus/validators.py | 20 ++++++++++--------- tests/components/modbus/test_init.py | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index dfec0dbb50a..cfda4a3863a 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -90,7 +90,6 @@ SERVICE_WRITE_REGISTER = "write_register" # integration names DEFAULT_HUB = "modbus_hub" -MINIMUM_SCAN_INTERVAL = 5 # seconds DEFAULT_SCAN_INTERVAL = 15 # seconds DEFAULT_SLAVE = 1 DEFAULT_STRUCTURE_PREFIX = ">f" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 0f376609de5..1b94010b5ef 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -26,7 +26,6 @@ from .const import ( DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCT_FORMAT, - MINIMUM_SCAN_INTERVAL, PLATFORMS, ) @@ -127,25 +126,28 @@ def scan_interval_validator(config: dict) -> dict: for entry in hub[conf_key]: scan_interval = entry.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval < MINIMUM_SCAN_INTERVAL: - if scan_interval == 0: - continue + if scan_interval == 0: + continue + if scan_interval < 5: _LOGGER.warning( - "%s %s scan_interval(%d) is adjusted to minimum(%d)", + "%s %s scan_interval(%d) is lower than 5 seconds, " + "which may cause Home Assistant stability issues", component, entry.get(CONF_NAME), scan_interval, - MINIMUM_SCAN_INTERVAL, ) - scan_interval = MINIMUM_SCAN_INTERVAL entry[CONF_SCAN_INTERVAL] = scan_interval minimum_scan_interval = min(scan_interval, minimum_scan_interval) - if CONF_TIMEOUT in hub and hub[CONF_TIMEOUT] > minimum_scan_interval - 1: + if ( + CONF_TIMEOUT in hub + and hub[CONF_TIMEOUT] > minimum_scan_interval - 1 + and minimum_scan_interval > 1 + ): _LOGGER.warning( "Modbus %s timeout(%d) is adjusted(%d) due to scan_interval", hub.get(CONF_NAME, ""), hub[CONF_TIMEOUT], minimum_scan_interval - 1, ) - hub[CONF_TIMEOUT] = minimum_scan_interval - 1 + hub[CONF_TIMEOUT] = minimum_scan_interval - 1 return config diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0e9bf43eec2..37cc8ae8766 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -457,9 +457,9 @@ async def mock_modbus_read_pymodbus( @pytest.mark.parametrize( "do_domain, do_group,do_type,do_scan_interval", [ - [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 1], - [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 1], - [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 1], + [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 10], + [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 10], + [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 10], [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_COIL, 1], ], ) From ede7932a5769fc936fdc3bf150e200aaca817f65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Jun 2021 09:14:18 -0700 Subject: [PATCH 177/750] Protect our user agent (#51486) * Protect our user agent * Fix expected error --- homeassistant/helpers/aiohttp_client.py | 8 +++++++- tests/helpers/test_aiohttp_client.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 0bb9a815c84..696f2d40cb8 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable from contextlib import suppress from ssl import SSLContext import sys +from types import MappingProxyType from typing import Any, Callable, cast import aiohttp @@ -95,9 +96,14 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) + # Prevent packages accidentally overriding our default headers + # It's important that we identify as Home Assistant + # If a package requires a different user agent, override it by passing a headers + # dictionary to the request method. + # pylint: disable=protected-access + clientsession._default_headers = MappingProxyType({USER_AGENT: SERVER_SOFTWARE}) # type: ignore clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index e6f113c7699..f68c7ba2181 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -195,3 +195,14 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie resp = await camera_client.get("/api/camera_proxy_stream/camera.config_test") assert resp.status == 502 + + +async def test_client_session_immutable_headers(hass): + """Test we can't mutate headers.""" + session = client.async_get_clientsession(hass) + + with pytest.raises(TypeError): + session.headers["user-agent"] = "bla" + + with pytest.raises(AttributeError): + session.headers.update({"user-agent": "bla"}) From 05241a7a682e1ff43785833cf370d06e450945f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Jun 2021 18:14:48 +0200 Subject: [PATCH 178/750] Allow number/sensor entities in numeric state conditions/triggers (#51439) --- homeassistant/helpers/config_validation.py | 2 +- .../triggers/test_numeric_state.py | 42 ++++++++++++++----- tests/helpers/test_condition.py | 8 ++-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 40457ef92c8..88f38f0a688 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -926,7 +926,7 @@ SERVICE_SCHEMA = vol.All( ) NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( - vol.Coerce(float), vol.All(str, entity_domain("input_number")) + vol.Coerce(float), vol.All(str, entity_domain(["input_number", "number", "sensor"])) ) CONDITION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string} diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 38372d09825..0e71594937f 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -47,9 +47,13 @@ async def setup_comp(hass): } }, ) + hass.states.async_set("number.value_10", 10) + hass.states.async_set("sensor.value_10", 10) -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_not_fires_on_entity_removal(hass, calls, below): """Test the firing with removed entity.""" hass.states.async_set("test.entity", 11) @@ -75,7 +79,9 @@ async def test_if_not_fires_on_entity_removal(hass, calls, below): assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_fires_on_entity_change_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -120,7 +126,9 @@ async def test_if_fires_on_entity_change_below(hass, calls, below): assert calls[0].data["id"] == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_fires_on_entity_change_over_to_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -147,7 +155,9 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls, below): assert len(calls) == 1 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_fires_on_entities_change_over_to_below(hass, calls, below): """Test the firing with changed entities.""" hass.states.async_set("test.entity_1", 11) @@ -178,7 +188,9 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls, below): assert len(calls) == 2 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below): """Test the firing with changed entity.""" context = Context() @@ -217,7 +229,9 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below): assert len(calls) == 1 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) @@ -244,7 +258,9 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below): assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_not_fires_on_initial_entity_below(hass, calls, below): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 9) @@ -271,7 +287,9 @@ async def test_if_not_fires_on_initial_entity_below(hass, calls, below): assert len(calls) == 0 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_not_fires_on_initial_entity_above(hass, calls, above): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 11) @@ -298,7 +316,9 @@ async def test_if_not_fires_on_initial_entity_above(hass, calls, above): assert len(calls) == 0 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize( + "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10") +) async def test_if_fires_on_entity_change_above(hass, calls, above): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 9) @@ -1632,8 +1652,8 @@ def test_below_above(): ) -def test_schema_input_number(): - """Test input_number only is accepted for above/below.""" +def test_schema_unacceptable_entities(): + """Test input_number, number & sensor only is accepted for above/below.""" with pytest.raises(vol.Invalid): numeric_state_trigger.TRIGGER_SCHEMA( { diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 2290ce9f679..973196d29f8 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1265,12 +1265,12 @@ async def test_numeric_state_attribute(hass): async def test_numeric_state_using_input_number(hass): """Test numeric_state conditions using input_number entities.""" + hass.states.async_set("number.low", 10) await async_setup_component( hass, "input_number", { "input_number": { - "low": {"min": 0, "max": 255, "initial": 10}, "high": {"min": 0, "max": 255, "initial": 100}, } }, @@ -1285,7 +1285,7 @@ async def test_numeric_state_using_input_number(hass): "condition": "numeric_state", "entity_id": "sensor.temperature", "below": "input_number.high", - "above": "input_number.low", + "above": "number.low", }, ], }, @@ -1317,10 +1317,10 @@ async def test_numeric_state_using_input_number(hass): ) assert test(hass) - hass.states.async_set("input_number.low", "unknown") + hass.states.async_set("number.low", "unknown") assert not test(hass) - hass.states.async_set("input_number.low", "unavailable") + hass.states.async_set("number.low", "unavailable") assert not test(hass) with pytest.raises(ConditionError): From 7bf45f7bf71a533699edc62265c2b84889d78f9a Mon Sep 17 00:00:00 2001 From: uchagani Date: Fri, 4 Jun 2021 14:45:08 -0400 Subject: [PATCH 179/750] Bump islamic-prayer-times to 0.0.5 (#51174) * Bump islamic-prayer-times to 0.0.5 * update manifest file * update requirements_all --- homeassistant/components/islamic_prayer_times/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index af6d09d0302..e72eb0a6da7 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -2,7 +2,7 @@ "domain": "islamic_prayer_times", "name": "Islamic Prayer Times", "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "requirements": ["prayer_times_calculator==0.0.3"], + "requirements": ["prayer_times_calculator==0.0.5"], "codeowners": ["@engrbm87"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 7ab48c49a43..edd404c21c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1179,7 +1179,7 @@ poolsense==0.0.8 praw==7.2.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.3 +prayer_times_calculator==0.0.5 # homeassistant.components.progettihwsw progettihwsw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d9b9cee4ac..3313fde6e55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -652,7 +652,7 @@ poolsense==0.0.8 praw==7.2.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.3 +prayer_times_calculator==0.0.5 # homeassistant.components.progettihwsw progettihwsw==0.1.1 From 89d90bfb1b118457b047dd64bf4498546ef4bfe0 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Fri, 4 Jun 2021 18:03:13 -0300 Subject: [PATCH 180/750] Use a single job to ping all devices in the Broadlink integration (#51466) --- homeassistant/components/broadlink/heartbeat.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/heartbeat.py b/homeassistant/components/broadlink/heartbeat.py index 282df3ae6a8..b4deffa5b81 100644 --- a/homeassistant/components/broadlink/heartbeat.py +++ b/homeassistant/components/broadlink/heartbeat.py @@ -44,11 +44,15 @@ class BroadlinkHeartbeat: """Send packets to feed watchdog timers.""" hass = self._hass config_entries = hass.config_entries.async_entries(DOMAIN) + hosts = {entry.data[CONF_HOST] for entry in config_entries} + await hass.async_add_executor_job(self.heartbeat, hosts) - for entry in config_entries: - host = entry.data[CONF_HOST] + @staticmethod + def heartbeat(hosts): + """Send packets to feed watchdog timers.""" + for host in hosts: try: - await hass.async_add_executor_job(blk.ping, host) + blk.ping(host) except OSError as err: _LOGGER.debug("Failed to send heartbeat to %s: %s", host, err) else: From 0cd07334388192e7ada765751462af5fdf8a19eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Jun 2021 12:20:41 -1000 Subject: [PATCH 181/750] Remove empty tests for ping now that the code in icmplib is used (#51454) --- tests/components/ping/test_init.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/components/ping/test_init.py diff --git a/tests/components/ping/test_init.py b/tests/components/ping/test_init.py deleted file mode 100644 index 05dc47e27d8..00000000000 --- a/tests/components/ping/test_init.py +++ /dev/null @@ -1 +0,0 @@ -"""Test ping id allocation.""" From 909140a7c69326dcd7d77419a98df30a43c166b8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 5 Jun 2021 00:23:17 +0000 Subject: [PATCH 182/750] [ci skip] Translation update --- homeassistant/components/zha/translations/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 37a708b100a..8c726fc349f 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -41,8 +41,8 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { - "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane z baterii niedost\u0119pne po (sekundach)", - "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z sieci niedost\u0119pne po (sekundach)", + "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", + "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "title": "Opcje og\u00f3lne" From 12ac4109f42043f4e2ff96eb3bad681e041a4328 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Jun 2021 21:23:51 -1000 Subject: [PATCH 183/750] Ensure ssdp can callback messages that do not have an ST (#51436) * Ensure ssdp can callback messages that do not have an ST Sonos sends unsolicited messages when the device reboots. We want to capture these to ensure we can recover the subscriptions as soon as the device reboots * Update homeassistant/components/ssdp/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/ssdp/__init__.py | 29 +++++--- tests/components/ssdp/test_init.py | 88 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 3cd2cb87f70..d03f8967311 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -153,7 +153,7 @@ class Scanner: ) -> None: """Initialize class.""" self.hass = hass - self.seen: set[tuple[str, str]] = set() + self.seen: set[tuple[str, str | None]] = set() self.cache: dict[tuple[str, str], Mapping[str, str]] = {} self._integration_matchers = integration_matchers self._cancel_scan: Callable[[], None] | None = None @@ -268,20 +268,28 @@ class Scanner: domains.add(domain) return domains + def _async_seen(self, header_st: str | None, header_location: str | None) -> bool: + """Check if we have seen a specific st and optional location.""" + if header_st is None: + return True + return (header_st, header_location) in self.seen + + def _async_see(self, header_st: str | None, header_location: str | None) -> None: + """Mark a specific st and optional location as seen.""" + if header_st is not None: + self.seen.add((header_st, header_location)) + async def _async_process_entry(self, headers: Mapping[str, str]) -> None: """Process SSDP entries.""" _LOGGER.debug("_async_process_entry: %s", headers) - if "st" not in headers or "location" not in headers: - return - h_st = headers["st"] - h_location = headers["location"] - key = (h_st, h_location) + h_st = headers.get("st") + h_location = headers.get("location") - if udn := _udn_from_usn(headers.get("usn")): + if h_st and (udn := _udn_from_usn(headers.get("usn"))): self.cache[(udn, h_st)] = headers callbacks = self._async_get_matching_callbacks(headers) - if key in self.seen and not callbacks: + if self._async_seen(h_st, h_location) and not callbacks: return assert self.description_manager is not None @@ -290,9 +298,10 @@ class Scanner: discovery_info = discovery_info_from_headers_and_request(info_with_req) _async_process_callbacks(callbacks, discovery_info) - if key in self.seen: + + if self._async_seen(h_st, h_location): return - self.seen.add(key) + self._async_see(h_st, h_location) for domain in self._async_matching_domains(info_with_req): _LOGGER.debug("Discovered %s at %s", domain, h_location) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 6b41544a384..2b527064ff8 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -478,6 +478,94 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): assert "Failed to callback info" in caplog.text +async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog): + """Test matching based on callback can handle unsolicited ssdp traffic without st.""" + aioclient_mock.get( + "http://10.6.9.12:1400/xml/device_description.xml", + text=""" + + + Paulus + + + """, + ) + mock_ssdp_response = { + "location": "http://10.6.9.12:1400/xml/device_description.xml", + "nt": "uuid:RINCON_1111BB963FD801400", + "nts": "ssdp:alive", + "server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)", + "usn": "uuid:RINCON_1111BB963FD801400", + "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd", + "x-rincon-bootseq": "250", + "bootid.upnp.org": "250", + "x-rincon-wifimode": "0", + "x-rincon-variant": "1", + "household.smartspeaker.audio": "Sonos_v3294823948542543534", + } + intergration_callbacks = [] + + @callback + def _async_intergration_callbacks(info): + intergration_callbacks.append(info) + + def _generate_fake_ssdp_listener(*args, **kwargs): + listener = SSDPListener(*args, **kwargs) + + async def _async_callback(*_): + await listener.async_callback(mock_ssdp_response) + + @callback + def _callback(*_): + hass.async_create_task(listener.async_callback(mock_ssdp_response)) + + listener.async_start = _async_callback + listener.async_search = _callback + return listener + + with patch( + "homeassistant.components.ssdp.SSDPListener", + new=_generate_fake_ssdp_listener, + ): + hass.state = CoreState.stopped + assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) + await hass.async_block_till_done() + ssdp.async_register_callback( + hass, + _async_intergration_callbacks, + {"nts": "ssdp:alive", "x-rincon-bootseq": MATCH_ALL}, + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.state = CoreState.running + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + assert hass.state == CoreState.running + + assert ( + len(intergration_callbacks) == 2 + ) # unsolicited callbacks without st are not cached + assert intergration_callbacks[0] == { + "UDN": "uuid:RINCON_1111BB963FD801400", + "bootid.upnp.org": "250", + "deviceType": "Paulus", + "household.smartspeaker.audio": "Sonos_v3294823948542543534", + "nt": "uuid:RINCON_1111BB963FD801400", + "nts": "ssdp:alive", + "ssdp_location": "http://10.6.9.12:1400/xml/device_description.xml", + "ssdp_server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)", + "ssdp_usn": "uuid:RINCON_1111BB963FD801400", + "x-rincon-bootseq": "250", + "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd", + "x-rincon-variant": "1", + "x-rincon-wifimode": "0", + } + assert "Failed to callback info" not in caplog.text + + async def test_scan_second_hit(hass, aioclient_mock, caplog): """Test matching on second scan.""" aioclient_mock.get( From 634f6ba77b3e6ac1979a4668440a732ab4cf631b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 5 Jun 2021 11:50:56 +0200 Subject: [PATCH 184/750] Fix missing Tibber power production (#51505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index b60d88e9814..660bbb741b0 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -45,6 +45,7 @@ SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" RT_SENSOR_MAP = { "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT, None], "power": ["power", DEVICE_CLASS_POWER, POWER_WATT, None], + "powerProduction": ["power production", DEVICE_CLASS_POWER, POWER_WATT, None], "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT, None], "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT, None], "accumulatedConsumption": [ From f2692d4eaa839a097a3133d2a1fa0537a233d1f6 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 5 Jun 2021 12:07:52 +0200 Subject: [PATCH 185/750] Bump garminconnect_aio to 0.1.4 (#51507) --- homeassistant/components/garmin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 2495249e4a4..22e115d0e06 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect_aio==0.1.1"], + "requirements": ["garminconnect_aio==0.1.4"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index edd404c21c9..a9d5ada7a63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.1 +garminconnect_aio==0.1.4 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3313fde6e55..aff3b21c39e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.1 +garminconnect_aio==0.1.4 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed From 06c9a508694c1a20c296774cc223cebe933b2e24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jun 2021 00:13:12 -1000 Subject: [PATCH 186/750] Handle missing options in foreign_key for MSSQL (#51503) --- homeassistant/components/recorder/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 8e6c4861739..02c74635f03 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -280,10 +280,10 @@ def _update_states_table_with_foreign_key_options(connection, engine): for foreign_key in inspector.get_foreign_keys(TABLE_STATES): if foreign_key["name"] and ( # MySQL/MariaDB will have empty options - not foreign_key["options"] + not foreign_key.get("options") or # Postgres will have ondelete set to None - foreign_key["options"].get("ondelete") is None + foreign_key.get("options", {}).get("ondelete") is None ): alters.append( { @@ -319,7 +319,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): for foreign_key in inspector.get_foreign_keys(table): if ( foreign_key["name"] - and foreign_key["options"].get("ondelete") + and foreign_key.get("options", {}).get("ondelete") and foreign_key["constrained_columns"] == columns ): drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) From b61c8ce03431f36887842363f03f644282498702 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Jun 2021 12:15:03 +0200 Subject: [PATCH 187/750] Disable gpmdp integration (#51509) --- homeassistant/components/gpmdp/manifest.json | 1 + requirements_all.txt | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index 2b65226b0c1..51fad8e9e71 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -1,6 +1,7 @@ { "domain": "gpmdp", "name": "Google Play Music Desktop Player (GPMDP)", + "disabled": "Integration has incompatible requirements.", "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": ["websocket-client==0.54.0"], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index a9d5ada7a63..fe25803bb8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2346,9 +2346,6 @@ waterfurnace==1.1.0 # homeassistant.components.cisco_webex_teams webexteamssdk==1.1.1 -# homeassistant.components.gpmdp -websocket-client==0.54.0 - # homeassistant.components.wiffi wiffi==1.0.1 From 7a6d067eb413633699b4cd37b679b7ec8bd27fe6 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Jun 2021 13:26:35 +0200 Subject: [PATCH 188/750] Bump mcstatus to 6.0.0 (#51517) --- homeassistant/components/minecraft_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 0c8df177fec..99a5ff3a463 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -3,7 +3,7 @@ "name": "Minecraft Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/minecraft_server", - "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], + "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==6.0.0"], "codeowners": ["@elmurato"], "quality_scale": "silver", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index fe25803bb8b..2ebf314604a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -933,7 +933,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==5.1.1 +mcstatus==6.0.0 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aff3b21c39e..138adf8008a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==5.1.1 +mcstatus==6.0.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From e73cdfab2f67d3a5d5c91a593c7f76cb25af908b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Jun 2021 13:43:39 +0200 Subject: [PATCH 189/750] Fix mysensors typing (#51518) * Fix device * Fix init * Fix gateway * Fix config flow * Fix helpers * Remove mysensors from typing ignore list --- .../components/mysensors/__init__.py | 8 +- .../components/mysensors/config_flow.py | 117 +++++++++--------- homeassistant/components/mysensors/device.py | 8 +- homeassistant/components/mysensors/gateway.py | 9 +- homeassistant/components/mysensors/helpers.py | 10 +- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 7 files changed, 80 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 9d23cfd24b6..3f36d6e96cc 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -42,7 +42,7 @@ from .const import ( DevId, SensorType, ) -from .device import MySensorsDevice, MySensorsEntity, get_mysensors_devices +from .device import MySensorsDevice, get_mysensors_devices from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway from .helpers import on_unload @@ -271,7 +271,7 @@ def setup_mysensors_platform( hass: HomeAssistant, domain: str, # hass platform name discovery_info: dict[str, list[DevId]], - device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsEntity]], + device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsDevice]], device_args: ( None | tuple ) = None, # extra arguments that will be given to the entity constructor @@ -302,11 +302,13 @@ def setup_mysensors_platform( if not gateway: _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id) continue - device_class_copy = device_class + if isinstance(device_class, dict): child = gateway.sensors[node_id].children[child_id] s_type = gateway.const.Presentation(child.type).name device_class_copy = device_class[s_type] + else: + device_class_copy = device_class args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type) devices[dev_id] = device_class_copy(*args_copy) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index ad260c3ab58..223d27a2a60 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.mysensors import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.data_entry_flow import RESULT_TYPE_FORM, FlowResult +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -111,7 +111,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Set up config flow.""" self._gw_type: str | None = None - async def async_step_import(self, user_input: dict[str, str] | None = None): + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Import a config entry. This method is called by async_setup and it has already @@ -131,12 +131,14 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL - result: dict[str, Any] = await self.async_step_user(user_input=user_input) - if result["type"] == RESULT_TYPE_FORM: - return self.async_abort(reason=next(iter(result["errors"].values()))) + result: FlowResult = await self.async_step_user(user_input=user_input) + if errors := result.get("errors"): + return self.async_abort(reason=next(iter(errors.values()))) return result - async def async_step_user(self, user_input: dict[str, str] | None = None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Create a config entry from frontend user input.""" schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} schema = vol.Schema(schema) @@ -158,9 +160,11 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_gw_serial(self, user_input: dict[str, str] | None = None): + async def async_step_gw_serial( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create config entry for a serial gateway.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: errors.update( await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input) @@ -187,7 +191,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="gw_serial", data_schema=schema, errors=errors ) - async def async_step_gw_tcp(self, user_input: dict[str, str] | None = None): + async def async_step_gw_tcp( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create a config entry for a tcp gateway.""" errors = {} if user_input is not None: @@ -225,7 +231,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return True return False - async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None): + async def async_step_gw_mqtt( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create a config entry for a mqtt gateway.""" errors = {} if user_input is not None: @@ -280,9 +288,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: + def _async_create_entry(self, user_input: dict[str, Any]) -> FlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", @@ -296,55 +302,52 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, gw_type: ConfGatewayType, errors: dict[str, str], - user_input: dict[str, str] | None = None, + user_input: dict[str, Any], ) -> dict[str, str]: """Validate parameters common to all gateway types.""" - if user_input is not None: - errors.update(_validate_version(user_input.get(CONF_VERSION))) + errors.update(_validate_version(user_input[CONF_VERSION])) - if gw_type != CONF_GATEWAY_TYPE_MQTT: - if gw_type == CONF_GATEWAY_TYPE_TCP: - verification_func = is_socket_address - else: - verification_func = is_serial_port + if gw_type != CONF_GATEWAY_TYPE_MQTT: + if gw_type == CONF_GATEWAY_TYPE_TCP: + verification_func = is_socket_address + else: + verification_func = is_serial_port - try: - await self.hass.async_add_executor_job( - verification_func, user_input.get(CONF_DEVICE) - ) - except vol.Invalid: - errors[CONF_DEVICE] = ( - "invalid_ip" - if gw_type == CONF_GATEWAY_TYPE_TCP - else "invalid_serial" - ) - if CONF_PERSISTENCE_FILE in user_input: - try: - is_persistence_file(user_input[CONF_PERSISTENCE_FILE]) - except vol.Invalid: - errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" - else: - real_persistence_path = user_input[ - CONF_PERSISTENCE_FILE - ] = self._normalize_persistence_file( - user_input[CONF_PERSISTENCE_FILE] - ) - for other_entry in self._async_current_entries(): - if CONF_PERSISTENCE_FILE not in other_entry.data: - continue - if real_persistence_path == self._normalize_persistence_file( - other_entry.data[CONF_PERSISTENCE_FILE] - ): - errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" - break + try: + await self.hass.async_add_executor_job( + verification_func, user_input.get(CONF_DEVICE) + ) + except vol.Invalid: + errors[CONF_DEVICE] = ( + "invalid_ip" + if gw_type == CONF_GATEWAY_TYPE_TCP + else "invalid_serial" + ) + if CONF_PERSISTENCE_FILE in user_input: + try: + is_persistence_file(user_input[CONF_PERSISTENCE_FILE]) + except vol.Invalid: + errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" + else: + real_persistence_path = user_input[ + CONF_PERSISTENCE_FILE + ] = self._normalize_persistence_file(user_input[CONF_PERSISTENCE_FILE]) + for other_entry in self._async_current_entries(): + if CONF_PERSISTENCE_FILE not in other_entry.data: + continue + if real_persistence_path == self._normalize_persistence_file( + other_entry.data[CONF_PERSISTENCE_FILE] + ): + errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" + break - for other_entry in self._async_current_entries(): - if _is_same_device(gw_type, user_input, other_entry): - errors["base"] = "already_configured" - break + for other_entry in self._async_current_entries(): + if _is_same_device(gw_type, user_input, other_entry): + errors["base"] = "already_configured" + break - # if no errors so far, try to connect - if not errors and not await try_connect(self.hass, user_input): - errors["base"] = "cannot_connect" + # if no errors so far, try to connect + if not errors and not await try_connect(self.hass, user_input): + errors["base"] = "cannot_connect" return errors diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index c1d8c431bc0..c066e633eaa 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -3,12 +3,13 @@ from __future__ import annotations from functools import partial import logging +from typing import Any from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity @@ -36,6 +37,8 @@ MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}" class MySensorsDevice: """Representation of a MySensors device.""" + hass: HomeAssistant + def __init__( self, gateway_id: GatewayId, @@ -51,9 +54,8 @@ class MySensorsDevice: self.child_id: int = child_id self.value_type: int = value_type # value_type as int. string variant can be looked up in gateway consts self.child_type = self._child.type - self._values = {} + self._values: dict[int, Any] = {} self._update_scheduled = False - self.hass = None @property def dev_id(self) -> DevId: diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index ec403e6e34b..c0a91fbdb08 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -66,7 +66,7 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool: +async def try_connect(hass: HomeAssistant, user_input: dict[str, Any]) -> bool: """Try to connect to a gateway and report if it worked.""" if user_input[CONF_DEVICE] == MQTT_COMPONENT: return True # dont validate mqtt. mqtt gateways dont send ready messages :( @@ -250,7 +250,6 @@ async def _discover_persistent_devices( hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Discover platforms for devices loaded via persistence file.""" - tasks = [] new_devices = defaultdict(list) for node_id in gateway.sensors: if not validate_node(gateway, node_id): @@ -263,8 +262,6 @@ async def _discover_persistent_devices( _LOGGER.debug("discovering persistent devices: %s", new_devices) for platform, dev_ids in new_devices.items(): discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids) - if tasks: - await asyncio.wait(tasks) async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): @@ -331,8 +328,8 @@ def _gw_callback_factory( msg_type = msg.gateway.const.MessageType(msg.type) msg_handler: Callable[ - [Any, GatewayId, Message], Coroutine[None] - ] = HANDLERS.get(msg_type.name) + [HomeAssistant, GatewayId, Message], Coroutine[Any, Any, None] + ] | None = HANDLERS.get(msg_type.name) if msg_handler is None: return diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 9a35f67d49b..54f173de3e3 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -176,11 +176,15 @@ def validate_child( ) -> defaultdict[str, list[DevId]]: """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" validated: defaultdict[str, list[DevId]] = defaultdict(list) - pres: IntEnum = gateway.const.Presentation - set_req: IntEnum = gateway.const.SetReq + pres: type[IntEnum] = gateway.const.Presentation + set_req: type[IntEnum] = gateway.const.SetReq child_type_name: SensorType | None = next( (member.name for member in pres if member.value == child.type), None ) + if not child_type_name: + _LOGGER.warning("Child type %s is not supported", child.type) + return validated + value_types: set[int] = {value_type} if value_type else {*child.values} value_type_names: set[ValueType] = { member.name for member in set_req if member.value in value_types @@ -199,7 +203,7 @@ def validate_child( child_value_names: set[ValueType] = { member.name for member in set_req if member.value in child.values } - v_names: set[ValueType] = platform_v_names & child_value_names + v_names = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) diff --git a/mypy.ini b/mypy.ini index 43468d5b173..7c2dbd38ccd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1197,9 +1197,6 @@ ignore_errors = true [mypy-homeassistant.components.mullvad.*] ignore_errors = true -[mypy-homeassistant.components.mysensors.*] -ignore_errors = true - [mypy-homeassistant.components.neato.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6310d0117c5..e9567be8924 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -127,7 +127,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.motion_blinds.*", "homeassistant.components.mqtt.*", "homeassistant.components.mullvad.*", - "homeassistant.components.mysensors.*", "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.*", From 62dd9d62cb812ccbd92bf6381dcdfb450477fa77 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Sat, 5 Jun 2021 14:11:39 +0200 Subject: [PATCH 190/750] Bump pyialarm to 1.8.1 (#51519) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index e112a26003e..08666129fd9 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.7"], + "requirements": ["pyialarm==1.8.1"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 2ebf314604a..0516c327020 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1463,7 +1463,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.7 +pyialarm==1.8.1 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 138adf8008a..a408a9f89a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -807,7 +807,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.7 +pyialarm==1.8.1 # homeassistant.components.icloud pyicloud==0.10.2 From b8afb7dcfe3d7be28a8165df1a571c3b294fbb07 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 5 Jun 2021 14:39:09 +0200 Subject: [PATCH 191/750] Check initial connect() worked in modbus (#51470) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/modbus/modbus.py | 29 +++++--- tests/components/modbus/test_init.py | 82 ++++++++++++++++++++--- 2 files changed, 92 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 4a02f019238..20e9978c0e8 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -65,7 +65,8 @@ async def async_modbus_setup( # modbus needs to be activated before components are loaded # to avoid a racing problem - await my_hub.async_setup() + if not await my_hub.async_setup(): + return False # load platforms for component, conf_key in PLATFORMS: @@ -195,8 +196,8 @@ class ModbusHub: }, } - def _log_error(self, exception_error: ModbusException, error_state=True): - log_text = "Pymodbus: " + str(exception_error) + def _log_error(self, text: str, error_state=True): + log_text = f"Pymodbus: {text}" if self._in_error: _LOGGER.debug(log_text) else: @@ -241,11 +242,13 @@ class ModbusHub: reset_socket=self._config_reset_socket, ) except ModbusException as exception_error: - self._log_error(exception_error, error_state=False) - return + self._log_error(str(exception_error), error_state=False) + return False async with self._lock: - await self.hass.async_add_executor_job(self._pymodbus_connect) + if not await self.hass.async_add_executor_job(self._pymodbus_connect): + self._log_error("initial connect failed, no retry", error_state=False) + return False self._call_type[CALL_TYPE_COIL][ENTRY_FUNC] = self._client.read_coils self._call_type[CALL_TYPE_DISCRETE][ @@ -271,6 +274,7 @@ class ModbusHub: self._async_cancel_listener = async_call_later( self.hass, self._config_delay, self.async_end_delay ) + return True @callback def async_end_delay(self, args): @@ -284,7 +288,7 @@ class ModbusHub: try: self._client.close() except ModbusException as exception_error: - self._log_error(exception_error) + self._log_error(str(exception_error)) self._client = None async def async_close(self): @@ -299,9 +303,10 @@ class ModbusHub: def _pymodbus_connect(self): """Connect client.""" try: - self._client.connect() + return self._client.connect() except ModbusException as exception_error: - self._log_error(exception_error, error_state=False) + self._log_error(str(exception_error), error_state=False) + return False def _pymodbus_call(self, unit, address, value, use_call): """Call sync. pymodbus.""" @@ -309,10 +314,10 @@ class ModbusHub: try: result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs) except ModbusException as exception_error: - self._log_error(exception_error) + self._log_error(str(exception_error)) result = exception_error if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]): - self._log_error(result) + self._log_error(str(result)) return None self._in_error = False return result @@ -321,6 +326,8 @@ class ModbusHub: """Convert async to sync pymodbus call.""" if self._config_delay: return None + if not self._client.is_socket_open(): + return None async with self._lock: return await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 37cc8ae8766..5f311ab57b0 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -511,15 +511,24 @@ async def test_pymodbus_constructor_fail(hass, caplog): ) as mock_pb: caplog.set_level(logging.ERROR) mock_pb.side_effect = ModbusException("test no class") - assert await async_setup_component(hass, DOMAIN, config) is True + assert await async_setup_component(hass, DOMAIN, config) is False await hass.async_block_till_done() - assert len(caplog.records) == 1 + assert caplog.messages[0].startswith("Pymodbus: Modbus Error: test") assert caplog.records[0].levelname == "ERROR" assert mock_pb.called -async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): - """Run test for failing pymodbus constructor.""" +@pytest.mark.parametrize( + "do_connect,do_exception,do_text", + [ + [False, None, "initial connect failed, no retry"], + [True, ModbusException("no connect"), "Modbus Error: no connect"], + ], +) +async def test_pymodbus_connect_fail( + hass, do_connect, do_exception, do_text, caplog, mock_pymodbus +): + """Run test for failing pymodbus connect.""" config = { DOMAIN: [ { @@ -530,12 +539,69 @@ async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): ] } caplog.set_level(logging.ERROR) - mock_pymodbus.connect.side_effect = ModbusException("test connect fail") - mock_pymodbus.close.side_effect = ModbusException("test connect fail") + mock_pymodbus.connect.return_value = do_connect + mock_pymodbus.connect.side_effect = do_exception + assert await async_setup_component(hass, DOMAIN, config) is False + await hass.async_block_till_done() + assert caplog.messages[0].startswith(f"Pymodbus: {do_text}") + assert caplog.records[0].levelname == "ERROR" + + +async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): + """Run test for failing pymodbus close.""" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + } + ] + } + caplog.set_level(logging.ERROR) + mock_pymodbus.connect.return_value = True + mock_pymodbus.close.side_effect = ModbusException("close fail") assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "ERROR" + # Close() is called as part of teardown + + +async def test_disconnect(hass, mock_pymodbus): + """Run test for startup delay.""" + + # the purpose of this test is to test a device disconnect + # We "hijiack" a binary_sensor to make a proper blackbox test. + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + CONF_BINARY_SENSORS: [ + { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_NAME: f"{TEST_SENSOR_NAME}", + CONF_ADDRESS: 52, + }, + ], + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + mock_pymodbus.is_socket_open.return_value = False + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + + # pass first scan_interval + now = now + timedelta(seconds=20) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_delay(hass, mock_pymodbus): From 59b5f94569afe66856ccde0e50cda1c195848723 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 5 Jun 2021 14:41:32 +0200 Subject: [PATCH 192/750] Add fix delay after send/request to allow RS485 adapter to switch in modbus (#51417) --- homeassistant/components/modbus/modbus.py | 6 +++++- tests/components/modbus/test_init.py | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 20e9978c0e8..f75c30b363b 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -329,6 +329,10 @@ class ModbusHub: if not self._client.is_socket_open(): return None async with self._lock: - return await self.hass.async_add_executor_job( + result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) + if self._config_type == "serial": + # small delay until next request/response + await asyncio.sleep(30 / 1000) + return result diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 5f311ab57b0..737c5ef2bb6 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -325,9 +325,13 @@ SERVICE = "service" [ { CONF_NAME: TEST_MODBUS_NAME, - CONF_TYPE: "tcp", - CONF_HOST: TEST_HOST, - CONF_PORT: 5501, + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, }, ], ) From 984695e99d85aa739eb499c9b2481ee44cf2710d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Jun 2021 20:02:32 +0200 Subject: [PATCH 193/750] Clean mysensors on_unload (#51521) * Clean mysensors on_unload * Fix docstring --- homeassistant/components/mysensors/__init__.py | 2 +- homeassistant/components/mysensors/binary_sensor.py | 7 ++++--- homeassistant/components/mysensors/climate.py | 7 ++++--- homeassistant/components/mysensors/cover.py | 5 +++-- homeassistant/components/mysensors/device_tracker.py | 8 +++++--- homeassistant/components/mysensors/gateway.py | 2 +- homeassistant/components/mysensors/helpers.py | 12 +++--------- homeassistant/components/mysensors/light.py | 7 ++++--- homeassistant/components/mysensors/sensor.py | 7 ++++--- homeassistant/components/mysensors/switch.py | 6 +++--- 10 files changed, 32 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 3f36d6e96cc..068fd9361fa 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -218,7 +218,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG], ) - await on_unload( + on_unload( hass, entry.entry_id, async_dispatcher_connect( diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 2077f38c758..8358c3b2ecf 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorEntity, ) -from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON @@ -18,6 +17,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .helpers import on_unload + SENSORS = { "S_DOOR": "door", "S_MOTION": DEVICE_CLASS_MOTION, @@ -48,9 +49,9 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - await on_unload( + on_unload( hass, - config_entry, + config_entry.entry_id, async_dispatcher_connect( hass, MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index f958f2274e0..797fcfafcc7 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -13,7 +13,6 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -21,6 +20,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .helpers import on_unload + DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", HVAC_MODE_COOL: "CoolOn", @@ -55,9 +56,9 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - await on_unload( + on_unload( hass, - config_entry, + config_entry.entry_id, async_dispatcher_connect( hass, MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 031efc97209..2d852ef05b4 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -4,7 +4,6 @@ import logging from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity -from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON @@ -12,6 +11,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .helpers import on_unload + _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,7 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - await on_unload( + on_unload( hass, config_entry.entry_id, async_dispatcher_connect( diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 45416ff7ae7..6297f8344fc 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,12 +1,14 @@ """Support for tracking MySensors devices.""" from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN -from homeassistant.components.mysensors import DevId, on_unload +from homeassistant.components.mysensors import DevId from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify +from .helpers import on_unload + async def async_setup_scanner( hass: HomeAssistant, config, async_see, discovery_info=None @@ -28,7 +30,7 @@ async def async_setup_scanner( for device in new_devices: gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID] dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type) - await on_unload( + on_unload( hass, gateway_id, async_dispatcher_connect( @@ -37,7 +39,7 @@ async def async_setup_scanner( device.async_update_callback, ), ) - await on_unload( + on_unload( hass, gateway_id, async_dispatcher_connect( diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index c0a91fbdb08..424f80df729 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -292,7 +292,7 @@ async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncG async def stop_this_gw(_: Event): await gw_stop(hass, entry, gateway) - await on_unload( + on_unload( hass, entry.entry_id, hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw), diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 54f173de3e3..5bc0a0d0839 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -10,7 +10,6 @@ from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -35,18 +34,13 @@ _LOGGER = logging.getLogger(__name__) SCHEMAS = Registry() -async def on_unload( - hass: HomeAssistant, entry: ConfigEntry | GatewayId, fnct: Callable -) -> None: +@callback +def on_unload(hass: HomeAssistant, gateway_id: GatewayId, fnct: Callable) -> None: """Register a callback to be called when entry is unloaded. This function is used by platforms to cleanup after themselves. """ - if isinstance(entry, GatewayId): - uniqueid = entry - else: - uniqueid = entry.entry_id - key = MYSENSORS_ON_UNLOAD.format(uniqueid) + key = MYSENSORS_ON_UNLOAD.format(gateway_id) if key not in hass.data[DOMAIN]: hass.data[DOMAIN][key] = [] hass.data[DOMAIN][key].append(fnct) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index aea99e3ee35..b9fbc139e4f 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -10,7 +10,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON @@ -20,6 +19,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list +from .helpers import on_unload + SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE @@ -45,9 +46,9 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - await on_unload( + on_unload( hass, - config_entry, + config_entry.entry_id, async_dispatcher_connect( hass, MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 48ab6e5d3a2..abbfa66ab8e 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -2,7 +2,6 @@ from awesomeversion import AwesomeVersion from homeassistant.components import mysensors -from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -27,6 +26,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .helpers import on_unload + SENSORS = { "V_TEMP": [None, "mdi:thermometer"], "V_HUM": [PERCENTAGE, "mdi:water-percent"], @@ -79,9 +80,9 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - await on_unload( + on_unload( hass, - config_entry, + config_entry.entry_id, async_dispatcher_connect( hass, MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 32a6a9a1202..3910df55eec 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -8,10 +8,10 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import on_unload from ...config_entries import ConfigEntry from ...helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE +from .helpers import on_unload ATTR_IR_CODE = "V_IR_SEND" @@ -83,9 +83,9 @@ async def async_setup_entry( schema=SEND_IR_CODE_SERVICE_SCHEMA, ) - await on_unload( + on_unload( hass, - config_entry, + config_entry.entry_id, async_dispatcher_connect( hass, MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN), From 8c00c24234269938f5aca332f78217dc8484c954 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jun 2021 12:02:36 -1000 Subject: [PATCH 194/750] Ensure host is always set with samsungtv SSDP discovery (#51527) There was a case where self._host could have been None before _async_set_unique_id_from_udn was called Fixes #51186 --- .../components/samsungtv/config_flow.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 7c6dea56b96..a69a456df40 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -112,6 +112,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_unique_id_from_udn(self, raise_on_progress=True): """Set the unique id from the udn.""" + assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) self._async_update_existing_host_entry(self._host) updates = {CONF_HOST: self._host} @@ -206,30 +207,28 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return entry return None - async def _async_start_discovery_for_host(self, host): - """Start discovery for a host.""" - if entry := self._async_update_existing_host_entry(host): + async def _async_start_discovery(self): + """Start discovery.""" + assert self._host is not None + if entry := self._async_update_existing_host_entry(self._host): if entry.unique_id: # Let the flow continue to fill the missing # unique id as we may be able to obtain it # in the next step raise data_entry_flow.AbortFlow("already_configured") - self.context[CONF_HOST] = host + self.context[CONF_HOST] = self._host for progress in self._async_in_progress(): - if progress.get("context", {}).get(CONF_HOST) == host: + if progress.get("context", {}).get(CONF_HOST) == self._host: raise data_entry_flow.AbortFlow("already_in_progress") - self._host = host - async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) + self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname await self._async_set_unique_id_from_udn() - await self._async_start_discovery_for_host( - urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - ) + await self._async_start_discovery() self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] if not self._manufacturer or not self._manufacturer.lower().startswith( "samsung" @@ -245,7 +244,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info[MAC_ADDRESS] - await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) + self._host = discovery_info[IP_ADDRESS] + await self._async_start_discovery() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -254,7 +254,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) - await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) + self._host = discovery_info[CONF_HOST] + await self._async_start_discovery() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() From c81df50191d55b637dc61717f2b4ffe01b0181ed Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 6 Jun 2021 00:19:43 +0000 Subject: [PATCH 195/750] [ci skip] Translation update --- .../components/abode/translations/he.json | 20 ++++++- .../accuweather/translations/he.json | 12 +++- .../components/acmeda/translations/he.json | 14 +++++ .../components/adguard/translations/he.json | 13 ++++- .../advantage_air/translations/he.json | 20 +++++++ .../components/aemet/translations/de.json | 9 +++ .../components/aemet/translations/he.json | 19 +++++++ .../components/agent_dvr/translations/he.json | 6 +- .../components/airly/translations/he.json | 14 ++++- .../components/airnow/translations/de.json | 3 +- .../components/airnow/translations/he.json | 21 +++++++ .../components/airvisual/translations/he.json | 18 +++++- .../alarmdecoder/translations/de.json | 32 ++++++++--- .../alarmdecoder/translations/he.json | 41 ++++++++++++++ .../components/almond/translations/he.json | 15 +++++ .../ambiclimate/translations/he.json | 8 +++ .../ambient_station/translations/he.json | 3 + .../components/apple_tv/translations/he.json | 11 ++++ .../components/arcam_fmj/translations/he.json | 21 +++++++ .../components/arcam_fmj/translations/it.json | 4 ++ .../components/asuswrt/translations/de.json | 6 +- .../components/asuswrt/translations/he.json | 20 +++++++ .../components/atag/translations/he.json | 18 ++++++ .../components/august/translations/he.json | 30 ++++++++++ .../components/aurora/translations/he.json | 11 ++++ .../components/awair/translations/he.json | 26 +++++++++ .../components/axis/translations/he.json | 9 ++- .../azure_devops/translations/he.json | 8 +++ .../binary_sensor/translations/he.json | 15 +++++ .../components/blebox/translations/he.json | 8 +++ .../components/blink/translations/he.json | 21 +++++++ .../bmw_connected_drive/translations/de.json | 10 ++++ .../bmw_connected_drive/translations/he.json | 19 +++++++ .../components/bond/translations/he.json | 20 +++++++ .../components/bosch_shc/translations/de.json | 19 ++++++- .../components/bosch_shc/translations/he.json | 32 +++++++++++ .../components/braviatv/translations/he.json | 18 ++++++ .../components/broadlink/translations/de.json | 14 ++++- .../components/broadlink/translations/he.json | 29 ++++++++++ .../components/brother/translations/he.json | 18 ++++++ .../components/bsblan/translations/he.json | 18 ++++++ .../buienradar/translations/de.json | 3 +- .../buienradar/translations/he.json | 18 ++++++ .../components/canary/translations/de.json | 4 +- .../components/canary/translations/he.json | 28 ++++++++++ .../components/cast/translations/he.json | 22 ++++++++ .../cert_expiry/translations/he.json | 19 +++++++ .../components/climacell/translations/he.json | 2 + .../components/cloud/translations/he.json | 9 +++ .../cloudflare/translations/he.json | 31 ++++++++++ .../components/control4/translations/he.json | 21 +++++++ .../coolmaster/translations/he.json | 14 +++++ .../components/cover/translations/he.json | 5 ++ .../components/daikin/translations/he.json | 11 ++++ .../components/deconz/translations/de.json | 2 +- .../components/deconz/translations/he.json | 15 ++++- .../components/demo/translations/it.json | 6 ++ .../components/denonavr/translations/he.json | 21 +++++++ .../devolo_home_control/translations/he.json | 9 ++- .../components/dexcom/translations/he.json | 15 +++++ .../dialogflow/translations/he.json | 7 +++ .../components/directv/translations/he.json | 19 +++++++ .../components/directv/translations/it.json | 4 ++ .../components/doorbird/translations/he.json | 7 +++ .../components/dsmr/translations/he.json | 7 +++ .../components/dsmr/translations/it.json | 8 +++ .../components/dunehd/translations/he.json | 19 +++++++ .../components/eafm/translations/de.json | 3 +- .../components/eafm/translations/he.json | 11 ++++ .../components/ecobee/translations/he.json | 11 ++++ .../components/econet/translations/de.json | 3 +- .../components/econet/translations/he.json | 21 +++++++ .../components/elgato/translations/he.json | 14 +++++ .../components/elkm1/translations/he.json | 6 ++ .../components/emonitor/translations/de.json | 5 ++ .../components/emonitor/translations/he.json | 14 +++++ .../emulated_roku/translations/he.json | 15 +++++ .../components/enocean/translations/de.json | 18 ++++++ .../components/enocean/translations/he.json | 7 +++ .../enphase_envoy/translations/he.json | 17 ++++++ .../components/epson/translations/he.json | 15 +++++ .../components/esphome/translations/he.json | 13 +++++ .../components/ezviz/translations/de.json | 2 + .../components/ezviz/translations/he.json | 35 ++++++++++++ .../fireservicerota/translations/he.json | 28 ++++++++++ .../components/firmata/translations/he.json | 7 +++ .../components/firmata/translations/it.json | 4 ++ .../flick_electric/translations/he.json | 1 + .../components/flo/translations/he.json | 13 +++++ .../components/flume/translations/he.json | 15 +++++ .../flunearyou/translations/he.json | 4 ++ .../forked_daapd/translations/he.json | 17 ++++++ .../components/foscam/translations/de.json | 1 + .../components/foscam/translations/he.json | 22 ++++++++ .../components/freebox/translations/he.json | 19 +++++++ .../components/fritz/translations/de.json | 25 ++++++--- .../components/fritz/translations/he.json | 56 +++++++++++++++++++ .../components/fritzbox/translations/de.json | 6 +- .../components/fritzbox/translations/he.json | 16 ++++++ .../fritzbox_callmonitor/translations/de.json | 2 +- .../fritzbox_callmonitor/translations/he.json | 13 +++++ .../garages_amsterdam/translations/de.json | 11 +++- .../garages_amsterdam/translations/he.json | 9 +++ .../garmin_connect/translations/he.json | 9 +++ .../components/gdacs/translations/he.json | 7 +++ .../components/geofency/translations/he.json | 7 +++ .../geonetnz_volcano/translations/he.json | 7 +++ .../components/gios/translations/he.json | 7 +++ .../components/glances/translations/he.json | 14 ++++- .../components/goalzero/translations/de.json | 7 ++- .../components/goalzero/translations/he.json | 19 +++++++ .../components/gogogate2/translations/de.json | 1 + .../components/gogogate2/translations/he.json | 21 +++++++ .../google_travel_time/translations/he.json | 29 ++++++++++ .../components/gree/translations/he.json | 13 +++++ .../growatt_server/translations/he.json | 16 ++++++ .../components/guardian/translations/de.json | 3 + .../components/habitica/translations/de.json | 5 +- .../components/habitica/translations/he.json | 17 ++++++ .../components/hangouts/translations/he.json | 8 ++- .../components/harmony/translations/he.json | 22 ++++++++ .../components/hassio/translations/he.json | 10 ++++ .../components/heos/translations/he.json | 12 ++++ .../hisense_aehw4a1/translations/he.json | 8 +++ .../components/hive/translations/he.json | 26 +++++++++ .../components/hlk_sw16/translations/he.json | 21 +++++++ .../home_connect/translations/he.json | 16 ++++++ .../home_plus_control/translations/he.json | 15 +++++ .../homeassistant/translations/he.json | 9 ++- .../components/homekit/translations/he.json | 10 ++++ .../homekit_controller/translations/de.json | 5 +- .../homematicip_cloud/translations/he.json | 12 ++-- .../huawei_lte/translations/he.json | 14 ++++- .../components/hue/translations/he.json | 11 ++++ .../huisbaasje/translations/he.json | 19 +++++++ .../humidifier/translations/he.json | 25 +++++++++ .../translations/de.json | 1 + .../translations/he.json | 8 +++ .../hvv_departures/translations/he.json | 1 + .../components/hyperion/translations/he.json | 24 ++++++++ .../components/ialarm/translations/he.json | 11 ++++ .../components/iaqualink/translations/he.json | 4 +- .../components/icloud/translations/he.json | 31 +++++++++- .../components/insteon/translations/he.json | 40 ++++++++++++- .../components/ios/translations/he.json | 4 +- .../components/ipp/translations/he.json | 12 ++++ .../components/iqvia/translations/he.json | 7 +++ .../components/isy994/translations/de.json | 5 +- .../components/isy994/translations/he.json | 10 +++- .../components/izone/translations/he.json | 8 +++ .../components/juicenet/translations/he.json | 7 +++ .../keenetic_ndms2/translations/de.json | 14 ++++- .../keenetic_ndms2/translations/he.json | 21 +++++++ .../components/kmtronic/translations/he.json | 17 ++++++ .../components/kodi/translations/de.json | 12 +++- .../components/kodi/translations/he.json | 24 ++++++++ .../components/konnected/translations/de.json | 1 + .../components/konnected/translations/he.json | 31 ++++++++++ .../components/konnected/translations/it.json | 4 +- .../kostal_plenticore/translations/de.json | 3 +- .../kostal_plenticore/translations/he.json | 20 +++++++ .../components/kraken/translations/de.json | 1 + .../components/kraken/translations/he.json | 12 ++++ .../components/kraken/translations/it.json | 8 +++ .../components/kulersky/translations/he.json | 13 +++++ .../components/life360/translations/he.json | 13 ++++- .../components/lifx/translations/he.json | 8 +++ .../components/light/translations/he.json | 11 ++++ .../components/litejet/translations/he.json | 3 + .../litterrobot/translations/he.json | 20 +++++++ .../components/local_ip/translations/he.json | 12 ++++ .../logi_circle/translations/he.json | 12 ++++ .../components/lovelace/translations/he.json | 8 +++ .../lutron_caseta/translations/de.json | 15 +++++ .../lutron_caseta/translations/he.json | 10 ++++ .../components/lyric/translations/he.json | 16 ++++++ .../components/mazda/translations/he.json | 23 ++++++++ .../media_player/translations/he.json | 18 +++++- .../components/melcloud/translations/he.json | 7 ++- .../components/met/translations/de.json | 3 + .../met_eireann/translations/de.json | 1 + .../meteo_france/translations/he.json | 11 ++++ .../meteoclimatic/translations/de.json | 20 +++++++ .../meteoclimatic/translations/he.json | 18 ++++++ .../components/metoffice/translations/he.json | 2 + .../components/mikrotik/translations/he.json | 9 +++ .../components/mill/translations/he.json | 18 ++++++ .../minecraft_server/translations/he.json | 18 ++++++ .../components/monoprice/translations/he.json | 18 ++++++ .../moon/translations/sensor.he.json | 14 +++++ .../motion_blinds/translations/he.json | 27 +++++++++ .../components/motioneye/translations/de.json | 4 ++ .../components/motioneye/translations/he.json | 25 +++++++++ .../components/mqtt/translations/de.json | 13 ++++- .../components/mqtt/translations/he.json | 20 ++++++- .../components/mutesync/translations/he.json | 15 +++++ .../components/myq/translations/he.json | 15 +++++ .../components/mysensors/translations/de.json | 15 +++++ .../components/mysensors/translations/he.json | 14 +++++ .../components/nam/translations/de.json | 3 +- .../components/nam/translations/he.json | 22 ++++++++ .../components/neato/translations/he.json | 22 ++++++++ .../components/nest/translations/he.json | 20 ++++++- .../components/netatmo/translations/de.json | 1 + .../components/netatmo/translations/he.json | 26 +++++++-- .../components/nexia/translations/de.json | 1 + .../components/nexia/translations/he.json | 12 +++- .../nightscout/translations/de.json | 4 +- .../nightscout/translations/he.json | 17 ++++++ .../components/notion/translations/he.json | 6 +- .../components/nuheat/translations/he.json | 8 +++ .../components/nuki/translations/de.json | 1 + .../components/nuki/translations/he.json | 27 +++++++++ .../components/number/translations/he.json | 8 +++ .../components/nut/translations/he.json | 9 +++ .../components/nzbget/translations/he.json | 13 +++++ .../components/omnilogic/translations/he.json | 20 +++++++ .../onboarding/translations/he.json | 7 +++ .../components/onewire/translations/he.json | 11 ++++ .../components/onvif/translations/he.json | 12 +++- .../opentherm_gw/translations/he.json | 26 +++++++++ .../components/openuv/translations/he.json | 4 +- .../openweathermap/translations/he.json | 19 ++++++- .../ovo_energy/translations/he.json | 24 ++++++++ .../components/owntracks/translations/de.json | 2 +- .../components/ozw/translations/he.json | 17 ++++++ .../panasonic_viera/translations/he.json | 25 +++++++++ .../philips_js/translations/he.json | 14 ++++- .../components/pi_hole/translations/he.json | 28 ++++++++++ .../components/picnic/translations/he.json | 22 ++++++++ .../components/plaato/translations/de.json | 4 +- .../components/plaato/translations/he.json | 7 +++ .../components/plex/translations/he.json | 22 ++++++++ .../components/plugwise/translations/he.json | 19 +++++++ .../plum_lightpad/translations/he.json | 18 ++++++ .../components/point/translations/he.json | 21 +++++++ .../components/poolsense/translations/he.json | 19 +++++++ .../components/powerwall/translations/de.json | 1 + .../components/powerwall/translations/he.json | 17 ++++++ .../progettihwsw/translations/he.json | 38 +++++++++++++ .../components/ps4/translations/he.json | 5 ++ .../components/rachio/translations/he.json | 19 +++++++ .../rainmachine/translations/he.json | 8 ++- .../recollect_waste/translations/he.json | 7 +++ .../components/remote/translations/he.json | 15 +++++ .../components/rfxtrx/translations/de.json | 4 +- .../components/rfxtrx/translations/he.json | 32 +++++++++++ .../components/rfxtrx/translations/it.json | 10 +++- .../components/ring/translations/he.json | 3 + .../components/risco/translations/de.json | 6 +- .../components/risco/translations/he.json | 31 ++++++++++ .../translations/de.json | 3 +- .../translations/he.json | 20 +++++++ .../components/roku/translations/he.json | 14 +++++ .../components/roku/translations/it.json | 8 +++ .../components/roomba/translations/de.json | 1 + .../components/roomba/translations/he.json | 32 ++++++++++- .../components/roon/translations/he.json | 15 +++++ .../ruckus_unleashed/translations/he.json | 5 ++ .../components/samsungtv/translations/de.json | 19 +++++-- .../components/samsungtv/translations/he.json | 25 +++++++++ .../screenlogic/translations/he.json | 19 +++++++ .../season/translations/sensor.he.json | 16 ++++++ .../components/sense/translations/he.json | 9 +++ .../components/sentry/translations/de.json | 16 ++++++ .../components/sentry/translations/he.json | 17 ++++++ .../components/sharkiq/translations/he.json | 25 +++++++++ .../components/shelly/translations/de.json | 2 +- .../components/shelly/translations/he.json | 28 ++++++++++ .../shopping_list/translations/he.json | 11 ++++ .../components/sia/translations/de.json | 50 +++++++++++++++++ .../components/sia/translations/he.json | 38 +++++++++++++ .../simplisafe/translations/he.json | 12 +++- .../components/sma/translations/he.json | 12 ++++ .../components/smappee/translations/he.json | 28 ++++++++++ .../smart_meter_texas/translations/he.json | 20 +++++++ .../components/smarthab/translations/he.json | 15 +++++ .../smartthings/translations/he.json | 10 ++++ .../components/smarttub/translations/he.json | 24 ++++++++ .../components/smhi/translations/he.json | 4 +- .../components/sms/translations/he.json | 12 ++++ .../components/solaredge/translations/he.json | 11 ++++ .../components/solarlog/translations/he.json | 17 ++++++ .../components/soma/translations/he.json | 11 ++++ .../components/somfy/translations/he.json | 13 +++++ .../somfy_mylink/translations/de.json | 13 ++++- .../somfy_mylink/translations/he.json | 20 +++++++ .../components/sonarr/translations/he.json | 24 ++++++++ .../components/songpal/translations/he.json | 6 ++ .../components/spider/translations/he.json | 19 +++++++ .../components/spotify/translations/de.json | 2 +- .../components/spotify/translations/he.json | 15 +++++ .../squeezebox/translations/de.json | 2 +- .../squeezebox/translations/he.json | 28 ++++++++++ .../srp_energy/translations/he.json | 20 +++++++ .../components/starline/translations/he.json | 9 ++- .../components/subaru/translations/de.json | 3 +- .../components/subaru/translations/he.json | 20 +++++++ .../components/syncthing/translations/he.json | 21 +++++++ .../components/syncthru/translations/he.json | 22 ++++++++ .../synology_dsm/translations/he.json | 24 +++++++- .../system_bridge/translations/de.json | 5 +- .../system_bridge/translations/he.json | 32 +++++++++++ .../components/tado/translations/he.json | 3 + .../components/tag/translations/he.json | 3 + .../components/tasmota/translations/de.json | 3 + .../components/tasmota/translations/he.json | 12 ++++ .../tellduslive/translations/he.json | 21 +++++++ .../components/tesla/translations/he.json | 2 +- .../components/tibber/translations/he.json | 2 + .../components/tile/translations/he.json | 18 ++++++ .../components/toon/translations/de.json | 3 + .../components/toon/translations/he.json | 8 +++ .../totalconnect/translations/de.json | 5 +- .../totalconnect/translations/he.json | 10 +++- .../components/tradfri/translations/he.json | 4 +- .../transmission/translations/he.json | 10 ++++ .../components/tuya/translations/de.json | 4 +- .../components/tuya/translations/he.json | 8 +++ .../twentemilieu/translations/he.json | 10 ++++ .../components/twilio/translations/he.json | 9 +++ .../components/twinkly/translations/he.json | 17 ++++++ .../components/unifi/translations/de.json | 3 +- .../components/unifi/translations/he.json | 41 +++++++++++++- .../components/unifi/translations/it.json | 6 ++ .../components/upb/translations/he.json | 4 ++ .../components/upcloud/translations/he.json | 12 ++++ .../components/upnp/translations/de.json | 10 ++++ .../components/upnp/translations/he.json | 12 ++++ .../components/upnp/translations/it.json | 4 ++ .../components/verisure/translations/he.json | 26 +++++++++ .../components/vesync/translations/he.json | 12 +++- .../components/vilfo/translations/he.json | 12 ++++ .../components/vizio/translations/he.json | 23 ++++++++ .../components/volumio/translations/he.json | 16 ++++++ .../components/wallbox/translations/de.json | 22 ++++++++ .../components/wallbox/translations/he.json | 21 +++++++ .../water_heater/translations/he.json | 13 +++++ .../waze_travel_time/translations/de.json | 23 +++++++- .../waze_travel_time/translations/he.json | 11 ++++ .../components/wilight/translations/de.json | 4 +- .../components/wilight/translations/he.json | 13 +++++ .../components/withings/translations/he.json | 20 +++++++ .../components/wled/translations/he.json | 14 +++++ .../components/wolflink/translations/he.json | 20 +++++++ .../wolflink/translations/sensor.de.json | 8 +++ .../wolflink/translations/sensor.he.json | 11 ++++ .../components/xbox/translations/he.json | 17 ++++++ .../xiaomi_aqara/translations/he.json | 24 ++++++++ .../xiaomi_miio/translations/he.json | 16 ++++++ .../components/yeelight/translations/de.json | 1 + .../components/yeelight/translations/he.json | 42 ++++++++++++++ .../components/zha/translations/de.json | 2 + .../components/zha/translations/he.json | 41 ++++++++++++++ .../components/zha/translations/it.json | 4 +- .../zodiac/translations/sensor.he.json | 18 ++++++ .../components/zone/translations/he.json | 2 +- .../zoneminder/translations/de.json | 13 ++++- .../zoneminder/translations/he.json | 25 +++++++++ .../components/zwave/translations/he.json | 13 +++++ .../components/zwave_js/translations/he.json | 32 +++++++++++ 361 files changed, 4915 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/acmeda/translations/he.json create mode 100644 homeassistant/components/advantage_air/translations/he.json create mode 100644 homeassistant/components/aemet/translations/he.json create mode 100644 homeassistant/components/airnow/translations/he.json create mode 100644 homeassistant/components/alarmdecoder/translations/he.json create mode 100644 homeassistant/components/almond/translations/he.json create mode 100644 homeassistant/components/ambiclimate/translations/he.json create mode 100644 homeassistant/components/apple_tv/translations/he.json create mode 100644 homeassistant/components/arcam_fmj/translations/he.json create mode 100644 homeassistant/components/asuswrt/translations/he.json create mode 100644 homeassistant/components/atag/translations/he.json create mode 100644 homeassistant/components/august/translations/he.json create mode 100644 homeassistant/components/aurora/translations/he.json create mode 100644 homeassistant/components/awair/translations/he.json create mode 100644 homeassistant/components/azure_devops/translations/he.json create mode 100644 homeassistant/components/blink/translations/he.json create mode 100644 homeassistant/components/bmw_connected_drive/translations/he.json create mode 100644 homeassistant/components/bond/translations/he.json create mode 100644 homeassistant/components/bosch_shc/translations/he.json create mode 100644 homeassistant/components/braviatv/translations/he.json create mode 100644 homeassistant/components/broadlink/translations/he.json create mode 100644 homeassistant/components/brother/translations/he.json create mode 100644 homeassistant/components/bsblan/translations/he.json create mode 100644 homeassistant/components/buienradar/translations/he.json create mode 100644 homeassistant/components/canary/translations/he.json create mode 100644 homeassistant/components/cert_expiry/translations/he.json create mode 100644 homeassistant/components/cloud/translations/he.json create mode 100644 homeassistant/components/cloudflare/translations/he.json create mode 100644 homeassistant/components/control4/translations/he.json create mode 100644 homeassistant/components/coolmaster/translations/he.json create mode 100644 homeassistant/components/denonavr/translations/he.json create mode 100644 homeassistant/components/dexcom/translations/he.json create mode 100644 homeassistant/components/dialogflow/translations/he.json create mode 100644 homeassistant/components/directv/translations/he.json create mode 100644 homeassistant/components/dsmr/translations/he.json create mode 100644 homeassistant/components/dunehd/translations/he.json create mode 100644 homeassistant/components/eafm/translations/he.json create mode 100644 homeassistant/components/ecobee/translations/he.json create mode 100644 homeassistant/components/econet/translations/he.json create mode 100644 homeassistant/components/elgato/translations/he.json create mode 100644 homeassistant/components/emonitor/translations/he.json create mode 100644 homeassistant/components/emulated_roku/translations/he.json create mode 100644 homeassistant/components/enocean/translations/he.json create mode 100644 homeassistant/components/enphase_envoy/translations/he.json create mode 100644 homeassistant/components/epson/translations/he.json create mode 100644 homeassistant/components/ezviz/translations/he.json create mode 100644 homeassistant/components/fireservicerota/translations/he.json create mode 100644 homeassistant/components/firmata/translations/he.json create mode 100644 homeassistant/components/flo/translations/he.json create mode 100644 homeassistant/components/forked_daapd/translations/he.json create mode 100644 homeassistant/components/foscam/translations/he.json create mode 100644 homeassistant/components/freebox/translations/he.json create mode 100644 homeassistant/components/fritz/translations/he.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/he.json create mode 100644 homeassistant/components/garages_amsterdam/translations/he.json create mode 100644 homeassistant/components/gdacs/translations/he.json create mode 100644 homeassistant/components/geofency/translations/he.json create mode 100644 homeassistant/components/geonetnz_volcano/translations/he.json create mode 100644 homeassistant/components/gios/translations/he.json create mode 100644 homeassistant/components/goalzero/translations/he.json create mode 100644 homeassistant/components/gogogate2/translations/he.json create mode 100644 homeassistant/components/google_travel_time/translations/he.json create mode 100644 homeassistant/components/gree/translations/he.json create mode 100644 homeassistant/components/growatt_server/translations/he.json create mode 100644 homeassistant/components/habitica/translations/he.json create mode 100644 homeassistant/components/harmony/translations/he.json create mode 100644 homeassistant/components/hassio/translations/he.json create mode 100644 homeassistant/components/heos/translations/he.json create mode 100644 homeassistant/components/hisense_aehw4a1/translations/he.json create mode 100644 homeassistant/components/hive/translations/he.json create mode 100644 homeassistant/components/hlk_sw16/translations/he.json create mode 100644 homeassistant/components/home_connect/translations/he.json create mode 100644 homeassistant/components/home_plus_control/translations/he.json create mode 100644 homeassistant/components/huisbaasje/translations/he.json create mode 100644 homeassistant/components/humidifier/translations/he.json create mode 100644 homeassistant/components/hyperion/translations/he.json create mode 100644 homeassistant/components/ialarm/translations/he.json create mode 100644 homeassistant/components/ipp/translations/he.json create mode 100644 homeassistant/components/iqvia/translations/he.json create mode 100644 homeassistant/components/izone/translations/he.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/he.json create mode 100644 homeassistant/components/kmtronic/translations/he.json create mode 100644 homeassistant/components/kodi/translations/he.json create mode 100644 homeassistant/components/konnected/translations/he.json create mode 100644 homeassistant/components/kostal_plenticore/translations/he.json create mode 100644 homeassistant/components/kraken/translations/he.json create mode 100644 homeassistant/components/kulersky/translations/he.json create mode 100644 homeassistant/components/lifx/translations/he.json create mode 100644 homeassistant/components/litterrobot/translations/he.json create mode 100644 homeassistant/components/local_ip/translations/he.json create mode 100644 homeassistant/components/logi_circle/translations/he.json create mode 100644 homeassistant/components/lovelace/translations/he.json create mode 100644 homeassistant/components/lyric/translations/he.json create mode 100644 homeassistant/components/mazda/translations/he.json create mode 100644 homeassistant/components/meteo_france/translations/he.json create mode 100644 homeassistant/components/meteoclimatic/translations/de.json create mode 100644 homeassistant/components/meteoclimatic/translations/he.json create mode 100644 homeassistant/components/mill/translations/he.json create mode 100644 homeassistant/components/minecraft_server/translations/he.json create mode 100644 homeassistant/components/monoprice/translations/he.json create mode 100644 homeassistant/components/moon/translations/sensor.he.json create mode 100644 homeassistant/components/motion_blinds/translations/he.json create mode 100644 homeassistant/components/motioneye/translations/he.json create mode 100644 homeassistant/components/mutesync/translations/he.json create mode 100644 homeassistant/components/mysensors/translations/he.json create mode 100644 homeassistant/components/nam/translations/he.json create mode 100644 homeassistant/components/neato/translations/he.json create mode 100644 homeassistant/components/nightscout/translations/he.json create mode 100644 homeassistant/components/nuki/translations/he.json create mode 100644 homeassistant/components/number/translations/he.json create mode 100644 homeassistant/components/nzbget/translations/he.json create mode 100644 homeassistant/components/omnilogic/translations/he.json create mode 100644 homeassistant/components/onboarding/translations/he.json create mode 100644 homeassistant/components/onewire/translations/he.json create mode 100644 homeassistant/components/opentherm_gw/translations/he.json create mode 100644 homeassistant/components/ovo_energy/translations/he.json create mode 100644 homeassistant/components/ozw/translations/he.json create mode 100644 homeassistant/components/panasonic_viera/translations/he.json create mode 100644 homeassistant/components/pi_hole/translations/he.json create mode 100644 homeassistant/components/picnic/translations/he.json create mode 100644 homeassistant/components/plaato/translations/he.json create mode 100644 homeassistant/components/plex/translations/he.json create mode 100644 homeassistant/components/plugwise/translations/he.json create mode 100644 homeassistant/components/plum_lightpad/translations/he.json create mode 100644 homeassistant/components/point/translations/he.json create mode 100644 homeassistant/components/poolsense/translations/he.json create mode 100644 homeassistant/components/powerwall/translations/he.json create mode 100644 homeassistant/components/progettihwsw/translations/he.json create mode 100644 homeassistant/components/rachio/translations/he.json create mode 100644 homeassistant/components/recollect_waste/translations/he.json create mode 100644 homeassistant/components/rfxtrx/translations/he.json create mode 100644 homeassistant/components/risco/translations/he.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/he.json create mode 100644 homeassistant/components/roku/translations/he.json create mode 100644 homeassistant/components/roon/translations/he.json create mode 100644 homeassistant/components/samsungtv/translations/he.json create mode 100644 homeassistant/components/screenlogic/translations/he.json create mode 100644 homeassistant/components/season/translations/sensor.he.json create mode 100644 homeassistant/components/sentry/translations/he.json create mode 100644 homeassistant/components/sharkiq/translations/he.json create mode 100644 homeassistant/components/shelly/translations/he.json create mode 100644 homeassistant/components/shopping_list/translations/he.json create mode 100644 homeassistant/components/sia/translations/de.json create mode 100644 homeassistant/components/sia/translations/he.json create mode 100644 homeassistant/components/sma/translations/he.json create mode 100644 homeassistant/components/smappee/translations/he.json create mode 100644 homeassistant/components/smart_meter_texas/translations/he.json create mode 100644 homeassistant/components/smarthab/translations/he.json create mode 100644 homeassistant/components/smarttub/translations/he.json create mode 100644 homeassistant/components/sms/translations/he.json create mode 100644 homeassistant/components/solaredge/translations/he.json create mode 100644 homeassistant/components/solarlog/translations/he.json create mode 100644 homeassistant/components/soma/translations/he.json create mode 100644 homeassistant/components/somfy/translations/he.json create mode 100644 homeassistant/components/sonarr/translations/he.json create mode 100644 homeassistant/components/spider/translations/he.json create mode 100644 homeassistant/components/spotify/translations/he.json create mode 100644 homeassistant/components/squeezebox/translations/he.json create mode 100644 homeassistant/components/srp_energy/translations/he.json create mode 100644 homeassistant/components/subaru/translations/he.json create mode 100644 homeassistant/components/syncthing/translations/he.json create mode 100644 homeassistant/components/syncthru/translations/he.json create mode 100644 homeassistant/components/system_bridge/translations/he.json create mode 100644 homeassistant/components/tag/translations/he.json create mode 100644 homeassistant/components/tasmota/translations/he.json create mode 100644 homeassistant/components/tellduslive/translations/he.json create mode 100644 homeassistant/components/tile/translations/he.json create mode 100644 homeassistant/components/toon/translations/he.json create mode 100644 homeassistant/components/twentemilieu/translations/he.json create mode 100644 homeassistant/components/twilio/translations/he.json create mode 100644 homeassistant/components/twinkly/translations/he.json create mode 100644 homeassistant/components/upcloud/translations/he.json create mode 100644 homeassistant/components/verisure/translations/he.json create mode 100644 homeassistant/components/vilfo/translations/he.json create mode 100644 homeassistant/components/vizio/translations/he.json create mode 100644 homeassistant/components/volumio/translations/he.json create mode 100644 homeassistant/components/wallbox/translations/de.json create mode 100644 homeassistant/components/wallbox/translations/he.json create mode 100644 homeassistant/components/water_heater/translations/he.json create mode 100644 homeassistant/components/waze_travel_time/translations/he.json create mode 100644 homeassistant/components/wilight/translations/he.json create mode 100644 homeassistant/components/withings/translations/he.json create mode 100644 homeassistant/components/wled/translations/he.json create mode 100644 homeassistant/components/wolflink/translations/he.json create mode 100644 homeassistant/components/wolflink/translations/sensor.he.json create mode 100644 homeassistant/components/xbox/translations/he.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/he.json create mode 100644 homeassistant/components/xiaomi_miio/translations/he.json create mode 100644 homeassistant/components/yeelight/translations/he.json create mode 100644 homeassistant/components/zodiac/translations/sensor.he.json create mode 100644 homeassistant/components/zoneminder/translations/he.json create mode 100644 homeassistant/components/zwave_js/translations/he.json diff --git a/homeassistant/components/abode/translations/he.json b/homeassistant/components/abode/translations/he.json index 6f4191da70d..17717573b68 100644 --- a/homeassistant/components/abode/translations/he.json +++ b/homeassistant/components/abode/translations/he.json @@ -1,10 +1,26 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + }, "user": { "data": { - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + }, + "title": "\u05d9\u05e9 \u05dc\u05de\u05dc\u05d0 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05e9\u05dc\u05da \u05dc\u05d0\u05d3\u05d5\u05d1\u05d9" } } } diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json index 4c49313d977..869e00ca064 100644 --- a/homeassistant/components/accuweather/translations/he.json +++ b/homeassistant/components/accuweather/translations/he.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/acmeda/translations/he.json b/homeassistant/components/acmeda/translations/he.json new file mode 100644 index 00000000000..498f322a7b0 --- /dev/null +++ b/homeassistant/components/acmeda/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "step": { + "user": { + "data": { + "id": "\u05de\u05d6\u05d4\u05d4 \u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json index 1471fd6603b..e2114d19d97 100644 --- a/homeassistant/components/adguard/translations/he.json +++ b/homeassistant/components/adguard/translations/he.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { - "host": "Host", + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "port": "\u05e4\u05d5\u05e8\u05d8" + "port": "\u05e4\u05d5\u05e8\u05d8", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } } diff --git a/homeassistant/components/advantage_air/translations/he.json b/homeassistant/components/advantage_air/translations/he.json new file mode 100644 index 00000000000..7c534baa977 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, + "description": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc- API \u05e9\u05dc \u05d4\u05d8\u05d0\u05d1\u05dc\u05d8 \u05e9\u05dc\u05da \u05d4\u05de\u05d5\u05ea\u05e7\u05df \u05e2\u05dc \u05d4\u05e7\u05d9\u05e8.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index d5312805722..2a4a927b90a 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -18,5 +18,14 @@ "title": "[void]" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Sammeln von Daten von AEMET-Wetterstationen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/he.json b/homeassistant/components/aemet/translations/he.json new file mode 100644 index 00000000000..5a7d693afec --- /dev/null +++ b/homeassistant/components/aemet/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json index 6268822a90a..e50d45e5608 100644 --- a/homeassistant/components/agent_dvr/translations/he.json +++ b/homeassistant/components/agent_dvr/translations/he.json @@ -3,10 +3,14 @@ "abort": { "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" }, + "error": { + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { - "host": "Host", + "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05d5\u05e8\u05d8" } } diff --git a/homeassistant/components/airly/translations/he.json b/homeassistant/components/airly/translations/he.json index 4c49313d977..8faa6ac2092 100644 --- a/homeassistant/components/airly/translations/he.json +++ b/homeassistant/components/airly/translations/he.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" - } + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + }, + "title": "\u05d0\u05d5\u05d5\u05e8\u05d9\u05e8\u05d9" } } } diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json index 8c2b47c1bd4..646369b6b61 100644 --- a/homeassistant/components/airnow/translations/de.json +++ b/homeassistant/components/airnow/translations/de.json @@ -14,7 +14,8 @@ "data": { "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" + "longitude": "L\u00e4ngengrad", + "radius": "Stationsradius (Meilen; optional)" }, "description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.", "title": "AirNow" diff --git a/homeassistant/components/airnow/translations/he.json b/homeassistant/components/airnow/translations/he.json new file mode 100644 index 00000000000..84ccc63e4db --- /dev/null +++ b/homeassistant/components/airnow/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 7fc0c2983df..b6e4a181168 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -1,13 +1,27 @@ { "config": { "error": { - "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7" + "general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7", + "location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, + "geography_by_name": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "node_pro": { "data": { + "ip_address": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } + }, + "description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4." } } } diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index aea85f49a59..4936cce4dfd 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -16,40 +16,58 @@ "device_path": "Ger\u00e4tepfad", "host": "Host", "port": "Port" - } + }, + "title": "Verbindungseinstellungen konfigurieren" }, "user": { "data": { "protocol": "Protokoll" - } + }, + "title": "W\u00e4hlen Sie das AlarmDecoder-Protokoll" } } }, "options": { + "error": { + "int": "Das Feld unten muss eine ganze Zahl sein.", + "loop_range": "RF Loop muss eine ganze Zahl zwischen 1 und 4 sein.", + "loop_rfid": "RF Loop kann nicht ohne RF Serial verwendet werden.", + "relay_inclusive": "Relaisadresse und Relaiskanal sind abh\u00e4ngig voneinander und m\u00fcssen zusammen aufgenommen werden." + }, "step": { "arm_settings": { "data": { - "alt_night_mode": "Alternativer Nachtmodus" - } + "alt_night_mode": "Alternativer Nachtmodus", + "auto_bypass": "Automatischer Bypass bei Scharfschaltung", + "code_arm_required": "Code f\u00fcr Scharfschaltung erforderlich" + }, + "title": "AlarmDecoder konfigurieren" }, "init": { "data": { "edit_select": "Bearbeiten" }, - "description": "Was m\u00f6chtest du bearbeiten?" + "description": "Was m\u00f6chtest du bearbeiten?", + "title": "AlarmDecoder konfigurieren" }, "zone_details": { "data": { + "zone_loop": "RF Loop", "zone_name": "Zonenname", "zone_relayaddr": "Relais-Adresse", + "zone_relaychan": "Relaiskanal", + "zone_rfid": "RF Serial", "zone_type": "Zonentyp" - } + }, + "description": "Geben Sie Details f\u00fcr Zone {zone_number} ein. Um Zone {zone_number} zu l\u00f6schen, lassen Sie Zonenname leer.", + "title": "AlarmDecoder konfigurieren" }, "zone_select": { "data": { "zone_number": "Zonennummer" }, - "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest." + "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest.", + "title": "AlarmDecoder konfigurieren" } } } diff --git a/homeassistant/components/alarmdecoder/translations/he.json b/homeassistant/components/alarmdecoder/translations/he.json new file mode 100644 index 00000000000..e130a1997b2 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/he.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "protocol": { + "data": { + "device_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df", + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, + "user": { + "data": { + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc" + } + } + } + }, + "options": { + "error": { + "relay_inclusive": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e1\u05e8 \u05d5\u05e2\u05e8\u05d5\u05e5 \u05de\u05de\u05e1\u05e8 \u05d4\u05dd \u05ea\u05dc\u05d5\u05d9\u05d9 \u05e7\u05d5\u05d3 \u05d5\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc \u05d0\u05d5\u05ea\u05dd \u05d9\u05d7\u05d3." + }, + "step": { + "init": { + "data": { + "edit_select": "\u05e2\u05e8\u05d5\u05da" + } + }, + "zone_details": { + "data": { + "zone_relaychan": "\u05e2\u05e8\u05d5\u05e5 \u05de\u05de\u05e1\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/he.json b/homeassistant/components/almond/translations/he.json new file mode 100644 index 00000000000..6aa9dd1d75f --- /dev/null +++ b/homeassistant/components/almond/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/he.json b/homeassistant/components/ambiclimate/translations/he.json new file mode 100644 index 00000000000..8b4db32d23a --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/he.json b/homeassistant/components/ambient_station/translations/he.json index f5afbca71c0..1ebf99c897c 100644 --- a/homeassistant/components/ambient_station/translations/he.json +++ b/homeassistant/components/ambient_station/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, "error": { "no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df" }, diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json new file mode 100644 index 00000000000..faef37945c1 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pair_with_pin": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/he.json b/homeassistant/components/arcam_fmj/translations/he.json new file mode 100644 index 00000000000..afd05f99cac --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Arcam FMJ \u05d1- '{host}' \u05dc-Home Assistant?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json index 24c9b99e7a8..2b99566888b 100644 --- a/homeassistant/components/arcam_fmj/translations/it.json +++ b/homeassistant/components/arcam_fmj/translations/it.json @@ -5,6 +5,10 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi" }, + "error": { + "one": "Pi\u00f9", + "other": "Altri" + }, "flow_title": "{host}", "step": { "confirm": { diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index 36699d95753..bf7e2230810 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -23,6 +23,7 @@ "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)", "username": "Benutzername" }, + "description": "Einstellen der erforderlichen Parameter f\u00fcr die Verbindung mit Ihrem Router.", "title": "" } } @@ -31,8 +32,11 @@ "step": { "init": { "data": { + "consider_home": "Sekunden, um ein Ger\u00e4t als 'abwesend' zu betrachten", + "dnsmasq": "Der Speicherort der dnsmasq.leases-Dateien im Router", "interface": "Schnittstelle, von der du Statistiken haben m\u00f6chtest (z.B. eth0, eth1 usw.)", - "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)" + "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)", + "track_unknown": "Unbekannte / unbenannte Ger\u00e4te tracken" }, "title": "AsusWRT Optionen" } diff --git a/homeassistant/components/asuswrt/translations/he.json b/homeassistant/components/asuswrt/translations/he.json new file mode 100644 index 00000000000..def73c48be3 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "pwd_and_ssh": "\u05e1\u05e4\u05e7 \u05e8\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH", + "pwd_or_ssh": "\u05d0\u05e0\u05d0 \u05e1\u05e4\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssh_key": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc SSH (\u05d1\u05de\u05e7\u05d5\u05dd \u05dc\u05e1\u05d9\u05e1\u05de\u05d4)", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/he.json b/homeassistant/components/atag/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/atag/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json new file mode 100644 index 00000000000..4462aa91cf7 --- /dev/null +++ b/homeassistant/components/august/translations/he.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_validate": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} ." + }, + "user_validate": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "validation": { + "description": "\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea {login_method} \u05e9\u05dc\u05da ({\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9}) \u05d5\u05d4\u05d6\u05df \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05d4\u05dc\u05df" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/he.json b/homeassistant/components/aurora/translations/he.json new file mode 100644 index 00000000000..a7a3d83cfa2 --- /dev/null +++ b/homeassistant/components/aurora/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json new file mode 100644 index 00000000000..f1c920db1cb --- /dev/null +++ b/homeassistant/components/awair/translations/he.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + }, + "user": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", + "email": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json index 3007c0e968c..1c9a7404efd 100644 --- a/homeassistant/components/axis/translations/he.json +++ b/homeassistant/components/axis/translations/he.json @@ -1,9 +1,16 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/azure_devops/translations/he.json b/homeassistant/components/azure_devops/translations/he.json new file mode 100644 index 00000000000..3409550386c --- /dev/null +++ b/homeassistant/components/azure_devops/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 5f4fb949b34..df79548f4a8 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -18,6 +18,10 @@ "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", "on": "\u05e0\u05de\u05d5\u05da" }, + "battery_charging": { + "off": "\u05dc\u05d0 \u05e0\u05d8\u05e2\u05df", + "on": "\u05e0\u05d8\u05e2\u05df" + }, "cold": { "off": "\u05e8\u05d2\u05d9\u05dc", "on": "\u05e7\u05b7\u05e8" @@ -42,6 +46,10 @@ "off": "\u05e8\u05d2\u05d9\u05dc", "on": "\u05d7\u05dd" }, + "light": { + "off": "\u05d0\u05d9\u05df \u05d0\u05d5\u05e8", + "on": "\u05d6\u05d5\u05d4\u05d4 \u05d0\u05d5\u05e8" + }, "lock": { "off": "\u05e0\u05e2\u05d5\u05dc", "on": "\u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc" @@ -54,6 +62,10 @@ "off": "\u05e0\u05e7\u05d9", "on": "\u05d6\u05d5\u05d4\u05d4" }, + "moving": { + "off": "\u05dc\u05d0 \u05d6\u05d6", + "on": "\u05e0\u05e2" + }, "occupancy": { "off": "\u05e0\u05e7\u05d9", "on": "\u05d6\u05d5\u05d4\u05d4" @@ -62,6 +74,9 @@ "off": "\u05e1\u05d2\u05d5\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, + "plug": { + "off": "\u05de\u05e0\u05d5\u05ea\u05e7" + }, "presence": { "off": "\u05dc\u05d0 \u05e0\u05d5\u05db\u05d7", "on": "\u05e0\u05d5\u05db\u05d7" diff --git a/homeassistant/components/blebox/translations/he.json b/homeassistant/components/blebox/translations/he.json index 001f8457f14..c904fe2bb04 100644 --- a/homeassistant/components/blebox/translations/he.json +++ b/homeassistant/components/blebox/translations/he.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blink/translations/he.json b/homeassistant/components/blink/translations/he.json new file mode 100644 index 00000000000..764c41136e2 --- /dev/null +++ b/homeassistant/components/blink/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json index d274719d7d0..85e27b5b3e4 100644 --- a/homeassistant/components/bmw_connected_drive/translations/de.json +++ b/homeassistant/components/bmw_connected_drive/translations/de.json @@ -16,5 +16,15 @@ } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Schreibgesch\u00fctzt (nur Sensoren und Notify, keine Ausf\u00fchrung von Diensten, kein Abschlie\u00dfen)", + "use_location": "Standort des Home Assistant f\u00fcr die Abfrage des Fahrzeugstandorts verwenden (erforderlich f\u00fcr nicht i3/i8 Fahrzeuge, die vor 7/2014 produziert wurden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/he.json b/homeassistant/components/bmw_connected_drive/translations/he.json new file mode 100644 index 00000000000..49f37a267d0 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/he.json b/homeassistant/components/bond/translations/he.json new file mode 100644 index 00000000000..3d38d2deaac --- /dev/null +++ b/homeassistant/components/bond/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 8d3e47d0ab6..110e986e106 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -7,17 +7,32 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "pairing_failed": "Pairing fehlgeschlagen; bitte pr\u00fcfen Sie, ob sich der Bosch Smart Home Controller im Pairing-Modus befindet (LED blinkt) und ob Ihr Passwort korrekt ist.", + "session_error": "Sitzungsfehler: API gab Non-OK-Ergebnis zur\u00fcck.", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Bosch SHC: {name}", "step": { + "confirm_discovery": { + "description": "Bitte dr\u00fccken Sie die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nSind Sie bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" + }, + "credentials": { + "data": { + "password": "Passwort des Smart Home Controllers" + } + }, "reauth_confirm": { + "description": "Die bosch_shc-Integration muss Ihr Konto neu authentifizieren", "title": "Integration erneut authentifizieren" }, "user": { "data": { "host": "Host" - } + }, + "description": "Richten Sie Ihren Bosch Smart Home Controller ein, um die \u00dcberwachung und Steuerung mit Home Assistant zu erm\u00f6glichen.", + "title": "SHC Authentifizierungsparameter" } } - } + }, + "title": "Bosch SHC" } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/he.json b/homeassistant/components/bosch_shc/translations/he.json new file mode 100644 index 00000000000..f7b240ce079 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "pairing_failed": "\u05d4\u05e9\u05d9\u05d5\u05da \u05e0\u05db\u05e9\u05dc; \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05e9\u05d1\u05e7\u05e8 \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd \u05e9\u05dc Bosch \u05e0\u05de\u05e6\u05d0 \u05d1\u05de\u05e6\u05d1 \u05e9\u05d9\u05d5\u05da (\u05de\u05d4\u05d1\u05d4\u05d1 LED) \u05db\u05de\u05d5 \u05d2\u05dd \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05e0\u05db\u05d5\u05e0\u05d4.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "confirm_discovery": { + "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e6\u05d3 \u05d4\u05e7\u05d3\u05de\u05d9 \u05e9\u05dc \u05d1\u05e7\u05e8\u05ea \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd \u05e9\u05dc Bosch \u05e2\u05d3 \u05e9\u05d4\u05e0\u05d5\u05e8\u05d9\u05ea \u05ea\u05ea\u05d7\u05d9\u05dc \u05dc\u05d4\u05d1\u05d4\u05d1.\n \u05de\u05d5\u05db\u05df \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05d5\u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} @ {host} \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea Home Assistant?" + }, + "credentials": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05ea \u05d1\u05e7\u05e8 \u05d4\u05d1\u05d9\u05ea \u05d4\u05d7\u05db\u05dd" + } + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json new file mode 100644 index 00000000000..280986d15ca --- /dev/null +++ b/homeassistant/components/braviatv/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index d1ab0987a3a..d81c131bf5d 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -13,6 +13,7 @@ "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{name} ({model} unter {host})", "step": { "auth": { "title": "Authentifiziere dich beim Ger\u00e4t" @@ -24,13 +25,22 @@ "title": "W\u00e4hle einen Namen f\u00fcr das Ger\u00e4t" }, "reset": { - "description": "{Name} ({Modell} unter {Host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre." + "description": "{name} ({model} unter {host}) ist gesperrt. Du musst das Ger\u00e4t entsperren, um dich zu authentifizieren und die Konfiguration abzuschlie\u00dfen. Anweisungen:\n1. \u00d6ffne die Broadlink-App.\n2. Klicke auf auf das Ger\u00e4t.\n3. Klicke oben rechts auf `...`.\n4. Scrolle zum unteren Ende der Seite.\n5. Deaktiviere die Sperre.", + "title": "Entsperren des Ger\u00e4ts" + }, + "unlock": { + "data": { + "unlock": "Ja mach das." + }, + "description": "{name} ({model} unter {host}) ist gesperrt. Dies kann zu Authentifizierungsproblemen im Home Assistant f\u00fchren. M\u00f6chten Sie es entsperren?", + "title": "Entsperren des Ger\u00e4ts (optional)" }, "user": { "data": { "host": "Host", "timeout": "Zeit\u00fcberschreitung" - } + }, + "title": "Verbinden mit dem Ger\u00e4t" } } } diff --git a/homeassistant/components/broadlink/translations/he.json b/homeassistant/components/broadlink/translations/he.json new file mode 100644 index 00000000000..a99f2f98761 --- /dev/null +++ b/homeassistant/components/broadlink/translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} ({model} \u05d1-{host})", + "step": { + "finish": { + "data": { + "name": "\u05e9\u05dd" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json new file mode 100644 index 00000000000..9f7a1b049b3 --- /dev/null +++ b/homeassistant/components/brother/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "wrong_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd." + }, + "flow_title": "{model} {serial_number}", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/he.json b/homeassistant/components/bsblan/translations/he.json new file mode 100644 index 00000000000..0e121bea5aa --- /dev/null +++ b/homeassistant/components/bsblan/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/de.json b/homeassistant/components/buienradar/translations/de.json index 72f1ebfed3c..bb09a98617d 100644 --- a/homeassistant/components/buienradar/translations/de.json +++ b/homeassistant/components/buienradar/translations/de.json @@ -20,7 +20,8 @@ "init": { "data": { "country_code": "L\u00e4ndercode des Landes, in dem Kamerabilder angezeigt werden sollen.", - "delta": "Zeitintervall in Sekunden zwischen Kamerabildaktualisierungen" + "delta": "Zeitintervall in Sekunden zwischen Kamerabildaktualisierungen", + "timeframe": "Minuten zum Vorausschauen f\u00fcr die Niederschlagsvorhersage" } } } diff --git a/homeassistant/components/buienradar/translations/he.json b/homeassistant/components/buienradar/translations/he.json new file mode 100644 index 00000000000..76da9d34ddf --- /dev/null +++ b/homeassistant/components/buienradar/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index bdd746c3149..081eb9126df 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -13,7 +13,8 @@ "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Mit Canary verbinden" } } }, @@ -21,6 +22,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "An ffmpeg \u00fcbergebene Argumente f\u00fcr Kameras", "timeout": "Anfrage-Timeout (Sekunden)" } } diff --git a/homeassistant/components/canary/translations/he.json b/homeassistant/components/canary/translations/he.json new file mode 100644 index 00000000000..9de2fb80888 --- /dev/null +++ b/homeassistant/components/canary/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d1\u05e7\u05e9\u05d4 (\u05e9\u05e0\u05d9\u05d5\u05ea)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 09c85008e10..1d3af8b2718 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -3,10 +3,32 @@ "abort": { "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Google Cast \u05e0\u05d7\u05d5\u05e6\u05d4." }, + "error": { + "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd." + }, "step": { + "config": { + "data": { + "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd" + }, + "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc." + }, "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd." + }, + "step": { + "basic_options": { + "data": { + "known_hosts": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd" + }, + "description": "\u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc \u05e9\u05de\u05d5\u05ea \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d9\u05e6\u05d5\u05e7\u05d9\u05dd, \u05d4\u05e9\u05ea\u05de\u05e9 \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 mDNS \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/he.json b/homeassistant/components/cert_expiry/translations/he.json new file mode 100644 index 00000000000..9b55d58684e --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "connection_refused": "\u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e0\u05d3\u05d7\u05d4 \u05d1\u05e2\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05de\u05d0\u05e8\u05d7", + "connection_timeout": "\u05d7\u05dc\u05e3 \u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05de\u05d0\u05e8\u05d7 \u05d6\u05d4", + "resolve_failed": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e4\u05e2\u05e0\u05d7 \u05de\u05d0\u05e8\u05d7 \u05d6\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/climacell/translations/he.json index 81a4b5c1fce..a29131a61d4 100644 --- a/homeassistant/components/climacell/translations/he.json +++ b/homeassistant/components/climacell/translations/he.json @@ -1,6 +1,8 @@ { "config": { "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/cloud/translations/he.json b/homeassistant/components/cloud/translations/he.json new file mode 100644 index 00000000000..9ea65e73c4e --- /dev/null +++ b/homeassistant/components/cloud/translations/he.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa \u05de\u05d5\u05e4\u05e2\u05dc\u05ea", + "google_enabled": "Google \u05de\u05d5\u05e4\u05e2\u05dc", + "logged_in": "\u05de\u05d7\u05d5\u05d1\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/he.json b/homeassistant/components/cloudflare/translations/he.json new file mode 100644 index 00000000000..445cf45325d --- /dev/null +++ b/homeassistant/components/cloudflare/translations/he.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_zone": "\u05d0\u05d6\u05d5\u05e8 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name}", + "step": { + "records": { + "data": { + "records": "\u05e8\u05e9\u05d5\u05de\u05d5\u05ea" + } + }, + "user": { + "data": { + "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + }, + "zone": { + "data": { + "zone": "\u05d0\u05b5\u05d6\u05d5\u05b9\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/he.json b/homeassistant/components/control4/translations/he.json new file mode 100644 index 00000000000..c7f019363ff --- /dev/null +++ b/homeassistant/components/control4/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/he.json b/homeassistant/components/coolmaster/translations/he.json new file mode 100644 index 00000000000..a109558689a --- /dev/null +++ b/homeassistant/components/coolmaster/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "no_units": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d9\u05d7\u05d9\u05d3\u05d5\u05ea HVAC \u05d1\u05de\u05d0\u05e8\u05d7 CoolMasterNet." + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index ebc7d39b450..fce73cc1698 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -1,4 +1,9 @@ { + "device_automation": { + "action_type": { + "stop": "\u05e2\u05e6\u05d5\u05e8 {entity_name}" + } + }, "state": { "_": { "closed": "\u05e0\u05e1\u05d2\u05e8", diff --git a/homeassistant/components/daikin/translations/he.json b/homeassistant/components/daikin/translations/he.json index 3007c0e968c..0bc64f684fd 100644 --- a/homeassistant/components/daikin/translations/he.json +++ b/homeassistant/components/daikin/translations/he.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 99d9e8d1e92..a24dbb44ad4 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -11,7 +11,7 @@ "error": { "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" }, - "flow_title": "deCONZ Zigbee Gateway", + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?", diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 163cd813dc3..434cb12dcb3 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -2,15 +2,28 @@ "config": { "abort": { "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ" + "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ", + "updated_instance": "\u05de\u05d5\u05e4\u05e2 deCONZ \u05e2\u05d5\u05d3\u05db\u05df \u05e2\u05dd \u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05d0\u05e8\u05d7\u05ea \u05d7\u05d3\u05e9\u05d4" }, "error": { "no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API" }, + "flow_title": "{host}", "step": { "link": { "description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea deCONZ \n .2 \u05dc\u05d7\u05e5 \u05e2\u05dc \"Unlock Gateway\"", "title": "\u05e7\u05e9\u05e8 \u05e2\u05dd deCONZ" + }, + "manual_input": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, + "user": { + "data": { + "host": "\u05d1\u05d7\u05e8 \u05e9\u05e2\u05e8 deCONZ \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4" + } } } } diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index dc3e218895b..27a068a1918 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,6 +1,12 @@ { "options": { "step": { + "init": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + } + }, "options_1": { "data": { "bool": "Valore booleano facoltativo", diff --git a/homeassistant/components/denonavr/translations/he.json b/homeassistant/components/denonavr/translations/he.json new file mode 100644 index 00000000000..3d080ab97dc --- /dev/null +++ b/homeassistant/components/denonavr/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_host": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05e9\u05dc \u05de\u05e7\u05dc\u05d8" + } + }, + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index ac90b3264ea..f9936c17225 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -4,7 +4,14 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9/ \u05de\u05d6\u05d4\u05d4 devolo" + } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 mydevolo", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc / \u05de\u05d6\u05d4\u05d4 devolo" } } } diff --git a/homeassistant/components/dexcom/translations/he.json b/homeassistant/components/dexcom/translations/he.json new file mode 100644 index 00000000000..837c2ac983b --- /dev/null +++ b/homeassistant/components/dexcom/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/he.json b/homeassistant/components/dialogflow/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/he.json b/homeassistant/components/directv/translations/he.json new file mode 100644 index 00000000000..f057c4e4629 --- /dev/null +++ b/homeassistant/components/directv/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index 2f8e5f29943..9fb0932c342 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -10,6 +10,10 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + }, "description": "Vuoi impostare {name} ?" }, "user": { diff --git a/homeassistant/components/doorbird/translations/he.json b/homeassistant/components/doorbird/translations/he.json index f08cbbdff11..5143667adfb 100644 --- a/homeassistant/components/doorbird/translations/he.json +++ b/homeassistant/components/doorbird/translations/he.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/dsmr/translations/he.json b/homeassistant/components/dsmr/translations/he.json new file mode 100644 index 00000000000..cdb921611c4 --- /dev/null +++ b/homeassistant/components/dsmr/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index 75cbb713056..f841ed32984 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -2,6 +2,14 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "one": "Pi\u00f9", + "other": "Altri" + }, + "step": { + "one": "Pi\u00f9", + "other": "Altri" } }, "options": { diff --git a/homeassistant/components/dunehd/translations/he.json b/homeassistant/components/dunehd/translations/he.json new file mode 100644 index 00000000000..5cf4da123b5 --- /dev/null +++ b/homeassistant/components/dunehd/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json index 9bb9fda51bf..46185acc11b 100644 --- a/homeassistant/components/eafm/translations/de.json +++ b/homeassistant/components/eafm/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_stations": "Keine Hochwassermessstellen gefunden." }, "step": { "user": { diff --git a/homeassistant/components/eafm/translations/he.json b/homeassistant/components/eafm/translations/he.json new file mode 100644 index 00000000000..ded87729985 --- /dev/null +++ b/homeassistant/components/eafm/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "station": "\u05ea\u05d7\u05e0\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/he.json b/homeassistant/components/ecobee/translations/he.json new file mode 100644 index 00000000000..3780025080f --- /dev/null +++ b/homeassistant/components/ecobee/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/de.json b/homeassistant/components/econet/translations/de.json index 854d61f1790..3b487d9f0e8 100644 --- a/homeassistant/components/econet/translations/de.json +++ b/homeassistant/components/econet/translations/de.json @@ -14,7 +14,8 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "title": "Rheem EcoNet-Konto einrichten" } } } diff --git a/homeassistant/components/econet/translations/he.json b/homeassistant/components/econet/translations/he.json new file mode 100644 index 00000000000..a881cd42615 --- /dev/null +++ b/homeassistant/components/econet/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/he.json b/homeassistant/components/elgato/translations/he.json new file mode 100644 index 00000000000..887a102c99a --- /dev/null +++ b/homeassistant/components/elgato/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json index ac90b3264ea..e85bab17ac0 100644 --- a/homeassistant/components/elkm1/translations/he.json +++ b/homeassistant/components/elkm1/translations/he.json @@ -1,9 +1,15 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json index 6abbe1b2b27..c36f7a5ae77 100644 --- a/homeassistant/components/emonitor/translations/de.json +++ b/homeassistant/components/emonitor/translations/de.json @@ -7,7 +7,12 @@ "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "title": "Einrichtung SiteSage Emonitor" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/emonitor/translations/he.json b/homeassistant/components/emonitor/translations/he.json new file mode 100644 index 00000000000..df463fa852c --- /dev/null +++ b/homeassistant/components/emonitor/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/he.json b/homeassistant/components/emulated_roku/translations/he.json new file mode 100644 index 00000000000..92608aacfa2 --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "host_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05de\u05d0\u05e8\u05d7\u05ea", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json index ea9858f470e..a8e4e2c7f84 100644 --- a/homeassistant/components/enocean/translations/de.json +++ b/homeassistant/components/enocean/translations/de.json @@ -1,7 +1,25 @@ { "config": { "abort": { + "invalid_dongle_path": "Ung\u00fcltiger Dongle-Pfad", "single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_dongle_path": "Kein g\u00fcltiger Dongle unter diesem Pfad gefunden" + }, + "step": { + "detect": { + "data": { + "path": "USB-Dongle-Pfad" + }, + "title": "W\u00e4hlen Sie den Pfad zu Ihrem ENOcean-Dongle" + }, + "manual": { + "data": { + "path": "USB-Dongle-Pfad" + }, + "title": "Geben Sie den Pfad zu Ihrem ENOcean-Dongle ein" + } } } } \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/he.json b/homeassistant/components/enocean/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/enocean/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/he.json b/homeassistant/components/enphase_envoy/translations/he.json new file mode 100644 index 00000000000..e2a84c257f3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/he.json b/homeassistant/components/epson/translations/he.json new file mode 100644 index 00000000000..33660936e12 --- /dev/null +++ b/homeassistant/components/epson/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json index 648d007cc46..5518029a18a 100644 --- a/homeassistant/components/esphome/translations/he.json +++ b/homeassistant/components/esphome/translations/he.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "authenticate": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d2\u05d3\u05e8\u05ea \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05e2\u05d1\u05d5\u05e8 {name}." + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json index 184255bcbcc..ab860d44201 100644 --- a/homeassistant/components/ezviz/translations/de.json +++ b/homeassistant/components/ezviz/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_account": "Konto wurde bereits konfiguriert", + "ezviz_cloud_account_missing": "Ezviz-Cloud-Konto fehlt. Bitte konfigurieren Sie das Ezviz-Cloud-Konto neu", "unknown": "Unerwarteter Fehler" }, "error": { @@ -42,6 +43,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "An ffmpeg \u00fcbergebene Argumente f\u00fcr Kameras", "timeout": "Anfrage-Timeout (Sekunden)" } } diff --git a/homeassistant/components/ezviz/translations/he.json b/homeassistant/components/ezviz/translations/he.json new file mode 100644 index 00000000000..56e85ef9641 --- /dev/null +++ b/homeassistant/components/ezviz/translations/he.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user_custom_url": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/he.json b/homeassistant/components/fireservicerota/translations/he.json new file mode 100644 index 00000000000..61dee20d1ce --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05d0\u05ea\u05e8 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/he.json b/homeassistant/components/firmata/translations/he.json new file mode 100644 index 00000000000..0a2ba64dbef --- /dev/null +++ b/homeassistant/components/firmata/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/it.json b/homeassistant/components/firmata/translations/it.json index a4f6f9e7222..b7eb09c2cb8 100644 --- a/homeassistant/components/firmata/translations/it.json +++ b/homeassistant/components/firmata/translations/it.json @@ -2,6 +2,10 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi" + }, + "step": { + "one": "Pi\u00f9", + "other": "Altri" } } } \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/he.json b/homeassistant/components/flick_electric/translations/he.json index 85688530711..658cdb97588 100644 --- a/homeassistant/components/flick_electric/translations/he.json +++ b/homeassistant/components/flick_electric/translations/he.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" }, "step": { diff --git a/homeassistant/components/flo/translations/he.json b/homeassistant/components/flo/translations/he.json new file mode 100644 index 00000000000..bb4fc738217 --- /dev/null +++ b/homeassistant/components/flo/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/he.json b/homeassistant/components/flume/translations/he.json index ac90b3264ea..0dec935b9a2 100644 --- a/homeassistant/components/flume/translations/he.json +++ b/homeassistant/components/flume/translations/he.json @@ -1,6 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05e2\u05d5\u05d3." + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json index 4c49313d977..f6195b11843 100644 --- a/homeassistant/components/flunearyou/translations/he.json +++ b/homeassistant/components/flunearyou/translations/he.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" } } diff --git a/homeassistant/components/forked_daapd/translations/he.json b/homeassistant/components/forked_daapd/translations/he.json new file mode 100644 index 00000000000..3b4e6e27cb3 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "wrong_host_or_port": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4.", + "wrong_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05ea API (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7 \u05d0\u05dd \u05d0\u05d9\u05df \u05e1\u05d9\u05e1\u05de\u05d4)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json index d87044b579a..bc1c12ea130 100644 --- a/homeassistant/components/foscam/translations/de.json +++ b/homeassistant/components/foscam/translations/de.json @@ -16,6 +16,7 @@ "password": "Passwort", "port": "Port", "rtsp_port": "RTSP-Port", + "stream": "Stream", "username": "Benutzername" } } diff --git a/homeassistant/components/foscam/translations/he.json b/homeassistant/components/foscam/translations/he.json new file mode 100644 index 00000000000..4f3eeb63e8c --- /dev/null +++ b/homeassistant/components/foscam/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/he.json b/homeassistant/components/freebox/translations/he.json new file mode 100644 index 00000000000..58521f503e2 --- /dev/null +++ b/homeassistant/components/freebox/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index d620396f1b5..dcded6750e9 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -12,23 +12,23 @@ "connection_error": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, - "flow_title": "FRITZ! Box Tools: {name}", + "flow_title": "FRITZ!Box Tools: {name}", "step": { "confirm": { "data": { "password": "Passwort", "username": "Benutzername" }, - "description": "Entdeckte FRITZ! Box: {name} \n\nRichte deine FRITZ! Box Tools ein, um {name} zu kontrollieren", - "title": "FRITZ! Box Tools einrichten" + "description": "Entdeckte FRITZ!Box: {name} \n\nRichte deine FRITZ!Box Tools ein, um {name} zu kontrollieren", + "title": "FRITZ!Box Tools einrichten" }, "reauth_confirm": { "data": { "password": "Passwort", "username": "Benutzername" }, - "description": "Aktualisiere die Anmeldeinformationen von FRITZ! Box Tools f\u00fcr: {host}. \n\nFRITZ! Box Tools kann sich nicht bei deiner FRITZ! Box anmelden.", - "title": "Aktualisieren der FRITZ! Box Tools - Anmeldeinformationen" + "description": "Aktualisiere die Anmeldeinformationen von FRITZ!Box Tools f\u00fcr: {host}. \n\nFRITZ!Box Tools kann sich nicht an deiner FRITZ!Box anmelden.", + "title": "Aktualisieren der FRITZ!Box Tools - Anmeldeinformationen" }, "start_config": { "data": { @@ -37,8 +37,8 @@ "port": "Port", "username": "Benutzername" }, - "description": "Einrichten der FRITZ! Box Tools zur Steuerung Ihrer FRITZ! Box.\n Ben\u00f6tigt: Benutzername, Passwort.", - "title": "Setup FRITZ! Box Tools - obligatorisch" + "description": "Einrichten der FRITZ!Box Tools zur Steuerung Ihrer FRITZ!Box.\n Ben\u00f6tigt: Benutzername, Passwort.", + "title": "Setup FRITZ!Box Tools - obligatorisch" }, "user": { "data": { @@ -46,6 +46,17 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" + }, + "description": "FRITZ!Box Tools einrichten, um Ihre FRITZ!Box zu steuern.\nMindestens erforderlich: Benutzername, Passwort.", + "title": "Setup FRITZ!Box Tools" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten" } } } diff --git a/homeassistant/components/fritz/translations/he.json b/homeassistant/components/fritz/translations/he.json new file mode 100644 index 00000000000..783f215cc40 --- /dev/null +++ b/homeassistant/components/fritz/translations/he.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "start_config": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u05e9\u05e0\u05d9\u05d5\u05ea \u05db\u05d3\u05d9 \u05dc\u05d4\u05d7\u05e9\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df \u05d1'\u05d1\u05d9\u05ea'" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 16263722482..ceaca6fd19a 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -4,13 +4,13 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.", + "not_supported": "Verbunden mit AVM FRITZ!Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Zugangsdaten" }, - "flow_title": "AVM FRITZ! Box: {name}", + "flow_title": "AVM FRITZ!Box: {name}", "step": { "confirm": { "data": { @@ -32,7 +32,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Gib deine AVM FRITZ! Box-Informationen ein." + "description": "Gib deine AVM FRITZ!Box-Informationen ein." } } } diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json index 035cb07a170..23be2761041 100644 --- a/homeassistant/components/fritzbox/translations/he.json +++ b/homeassistant/components/fritzbox/translations/he.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -7,8 +16,15 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/de.json b/homeassistant/components/fritzbox_callmonitor/translations/de.json index a26f301a9bd..b48ec34a030 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/de.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/de.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" }, - "flow_title": "AVM FRITZ! Box-Anrufmonitor: {name}", + "flow_title": "AVM FRITZ!Box-Anrufmonitor: {name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/he.json b/homeassistant/components/fritzbox_callmonitor/translations/he.json new file mode 100644 index 00000000000..bb4fc738217 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/de.json b/homeassistant/components/garages_amsterdam/translations/de.json index 71ade34b066..aa13e22eabb 100644 --- a/homeassistant/components/garages_amsterdam/translations/de.json +++ b/homeassistant/components/garages_amsterdam/translations/de.json @@ -4,6 +4,15 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "garage_name": "Name der Garage" + }, + "title": "W\u00e4hlen Sie eine Garage zur \u00dcberwachung aus" + } } - } + }, + "title": "Garages Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/he.json b/homeassistant/components/garages_amsterdam/translations/he.json new file mode 100644 index 00000000000..64404003783 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/he.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/he.json b/homeassistant/components/garmin_connect/translations/he.json index ac90b3264ea..e7bab78fd58 100644 --- a/homeassistant/components/garmin_connect/translations/he.json +++ b/homeassistant/components/garmin_connect/translations/he.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "too_many_requests": "\u05d1\u05e7\u05e9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05d3\u05d9, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gdacs/translations/he.json b/homeassistant/components/gdacs/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/gdacs/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/he.json b/homeassistant/components/geofency/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/geofency/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/he.json b/homeassistant/components/geonetnz_volcano/translations/he.json new file mode 100644 index 00000000000..59a1fbe0eed --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/he.json b/homeassistant/components/gios/translations/he.json new file mode 100644 index 00000000000..59a1fbe0eed --- /dev/null +++ b/homeassistant/components/gios/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/he.json b/homeassistant/components/glances/translations/he.json index 6f4191da70d..f5ba6464a4e 100644 --- a/homeassistant/components/glances/translations/he.json +++ b/homeassistant/components/glances/translations/he.json @@ -1,9 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } } diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 6b88f0a1209..f740d6fdfec 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -11,12 +11,17 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "confirm_discovery": { + "description": "Eine DHCP-Reservierung auf Ihrem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlagen Sie im Benutzerhandbuch Ihres Routers nach.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", "name": "Name" }, - "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach." + "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach.", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/goalzero/translations/he.json b/homeassistant/components/goalzero/translations/he.json new file mode 100644 index 00000000000..2e549971b52 --- /dev/null +++ b/homeassistant/components/goalzero/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json index 5c0173a99cf..1ccb678e5c2 100644 --- a/homeassistant/components/gogogate2/translations/de.json +++ b/homeassistant/components/gogogate2/translations/de.json @@ -7,6 +7,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/he.json b/homeassistant/components/gogogate2/translations/he.json new file mode 100644 index 00000000000..53c14104022 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{device} ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json new file mode 100644 index 00000000000..ba750745154 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "destination": "\u05d9\u05e2\u05d3", + "name": "\u05e9\u05dd", + "origin": "\u05de\u05e7\u05d5\u05e8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u05e9\u05e4\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/he.json b/homeassistant/components/gree/translations/he.json new file mode 100644 index 00000000000..d3d68dccc93 --- /dev/null +++ b/homeassistant/components/gree/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/he.json b/homeassistant/components/growatt_server/translations/he.json new file mode 100644 index 00000000000..cde5cec4fa4 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 432afe8df27..fc3ca8fee06 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -6,6 +6,9 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { + "discovery_confirm": { + "description": "M\u00f6chten Sie dieses Guardian-Ger\u00e4t einrichten?" + }, "user": { "data": { "ip_address": "IP-Adresse", diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json index 04f985946fb..ad4f3d2aff8 100644 --- a/homeassistant/components/habitica/translations/de.json +++ b/homeassistant/components/habitica/translations/de.json @@ -8,8 +8,11 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", + "api_user": "Habitica API-Benutzer-ID", + "name": "Override f\u00fcr den Benutzernamen von Habitica. Wird f\u00fcr Serviceaufrufe verwendet", "url": "URL" - } + }, + "description": "Verbinden Sie Ihr Habitica-Profil, um die \u00dcberwachung des Profils und der Aufgaben Ihres Benutzers zu erm\u00f6glichen. Beachten Sie, dass api_id und api_key von https://habitica.com/user/settings/api bezogen werden m\u00fcssen." } } }, diff --git a/homeassistant/components/habitica/translations/he.json b/homeassistant/components/habitica/translations/he.json new file mode 100644 index 00000000000..9ef8ea8a345 --- /dev/null +++ b/homeassistant/components/habitica/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_credentials": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e2\u05e7\u05d5\u05e3 \u05e2\u05d1\u05d5\u05e8 \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc Habitica. \u05d9\u05e9\u05de\u05e9 \u05e2\u05d1\u05d5\u05e8 \u05e7\u05e8\u05d9\u05d0\u05d5\u05ea \u05e9\u05d9\u05e8\u05d5\u05ea", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/he.json b/homeassistant/components/hangouts/translations/he.json index c3863a860f4..fa756c49ac6 100644 --- a/homeassistant/components/hangouts/translations/he.json +++ b/homeassistant/components/hangouts/translations/he.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", @@ -14,13 +14,15 @@ "data": { "2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" }, + "description": "\u05e8\u05d9\u05e7", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9" }, "user": { "data": { - "email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc", + "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, + "description": "\u05e8\u05d9\u05e7", "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts" } } diff --git a/homeassistant/components/harmony/translations/he.json b/homeassistant/components/harmony/translations/he.json new file mode 100644 index 00000000000..1331c17e961 --- /dev/null +++ b/homeassistant/components/harmony/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "link": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json new file mode 100644 index 00000000000..17b7fcd0050 --- /dev/null +++ b/homeassistant/components/hassio/translations/he.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "host_os": "\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d0\u05e8\u05d7\u05ea", + "supervisor_api": "API \u05e9\u05dc \u05de\u05e4\u05e7\u05d7", + "supervisor_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e4\u05e7\u05d7", + "update_channel": "\u05e2\u05e8\u05d5\u05e5 \u05e2\u05d3\u05db\u05d5\u05df" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/he.json b/homeassistant/components/heos/translations/he.json new file mode 100644 index 00000000000..92b9c1c48b2 --- /dev/null +++ b/homeassistant/components/heos/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05ea\u05e7\u05df Heos (\u05e8\u05e6\u05d5\u05d9 \u05db\u05d6\u05d4 \u05d4\u05de\u05d7\u05d5\u05d1\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d7\u05d5\u05d8 \u05dc\u05e8\u05e9\u05ea)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/he.json b/homeassistant/components/hisense_aehw4a1/translations/he.json new file mode 100644 index 00000000000..380dbc5d7fc --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/he.json b/homeassistant/components/hive/translations/he.json new file mode 100644 index 00000000000..3552485d234 --- /dev/null +++ b/homeassistant/components/hive/translations/he.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "invalid_password": "\u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc-Hive \u05e0\u05db\u05e9\u05dc\u05d4. \u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4. \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05e0\u05d9\u05ea.", + "invalid_username": "\u05d4\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05db\u05d5\u05d5\u05e8\u05ea \u05e0\u05db\u05e9\u05dc\u05d4. \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\u05e8 \u05d4\u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9 \u05e9\u05dc\u05da \u05d0\u05d9\u05e0\u05d4 \u05de\u05d6\u05d5\u05d4\u05d4.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/he.json b/homeassistant/components/hlk_sw16/translations/he.json new file mode 100644 index 00000000000..479d2f2f5e8 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/he.json b/homeassistant/components/home_connect/translations/he.json new file mode 100644 index 00000000000..6051eb96eb2 --- /dev/null +++ b/homeassistant/components/home_connect/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/he.json b/homeassistant/components/home_plus_control/translations/he.json new file mode 100644 index 00000000000..f39370d6cb6 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json index f45b17b1a13..f86d7b0dca0 100644 --- a/homeassistant/components/homeassistant/translations/he.json +++ b/homeassistant/components/homeassistant/translations/he.json @@ -1,7 +1,14 @@ { "system_health": { "info": { - "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4" + "docker": "Docker", + "hassio": "\u05de\u05e4\u05e7\u05d7", + "installation_type": "\u05e1\u05d5\u05d2 \u05d4\u05ea\u05e7\u05e0\u05d4", + "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4", + "os_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4", + "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df", + "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df", + "version": "\u05d2\u05d9\u05e8\u05e1\u05d4" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index cb5a530b739..789298b7705 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "include_domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc" + } + } + } + }, "options": { "step": { "include_exclude": { @@ -9,6 +18,7 @@ }, "init": { "data": { + "include_domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc", "mode": "\u05de\u05e6\u05d1" } }, diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 120e9a63e66..e8d3b4b55e4 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Ung\u00fcltiger HomeKit Code, \u00fcberpr\u00fcfe bitte den Code und versuche es erneut.", + "insecure_setup_code": "Der angeforderte Setup-Code ist unsicher, da er zu trivial ist. Dieses Zubeh\u00f6r erf\u00fcllt nicht die grundlegenden Sicherheitsanforderungen.", "max_peers_error": "Das Ger\u00e4t weigerte sich, die Kopplung durchzuf\u00fchren, da es keinen freien Kopplungs-Speicher hat.", "pairing_failed": "Beim Versuch dieses Ger\u00e4t zu koppeln ist ein Fehler aufgetreten. Dies kann ein vor\u00fcbergehender Fehler sein oder das Ger\u00e4t wird derzeit m\u00f6glicherweise nicht unterst\u00fctzt.", "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", @@ -29,12 +30,14 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.", "pairing_code": "Kopplungscode" }, - "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", + "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Geben Sie Ihren HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { + "description": "Das Ger\u00e4t befindet sich m\u00f6glicherweise nicht im Pairing-Modus und erfordert einen physischen oder virtuellen Tastendruck. Stellen Sie sicher, dass sich das Ger\u00e4t im Pairing-Modus befindet, oder versuchen Sie, das Ger\u00e4t neu zu starten und fahren Sie dann das Pairing fort.", "title": "Fehler bei der Kommunikation mit dem Zubeh\u00f6r" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json index f07db79a1c5..b6a75868d3b 100644 --- a/homeassistant/components/homematicip_cloud/translations/he.json +++ b/homeassistant/components/homematicip_cloud/translations/he.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05d2\u05d9\u05e9\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea", - "connection_aborted": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e8\u05ea HMIP", - "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "connection_aborted": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "invalid_sgtin_or_pin": "PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_sgtin_or_pin": "SGTIN \u05d0\u05d5 \u05e7\u05d5\u05d3 PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "press_the_button": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc.", "register_failed": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", "timeout_button": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1" @@ -15,8 +15,8 @@ "init": { "data": { "hapid": "\u05de\u05d6\u05d4\u05d4 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 (SGTIN)", - "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05de\u05e9\u05de\u05e9 \u05db\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05db\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd)", - "pin": "\u05e7\u05d5\u05d3 PIN (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05de\u05e9\u05de\u05e9 \u05db\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05db\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd)", + "pin": "\u05e7\u05d5\u05d3 PIN" }, "title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 HomematicIP" }, diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json index 6f4191da70d..9b1aa61ecd2 100644 --- a/homeassistant/components/huawei_lte/translations/he.json +++ b/homeassistant/components/huawei_lte/translations/he.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "error": { + "incorrect_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4", + "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "description": "\u05d4\u05d6\u05df \u05e4\u05e8\u05d8\u05d9 \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d4\u05ea\u05e7\u05df. \u05e6\u05d9\u05d5\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9, \u05d0\u05da \u05de\u05d0\u05e4\u05e9\u05e8 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05e0\u05d5\u05e1\u05e4\u05d5\u05ea. \u05de\u05e6\u05d3 \u05e9\u05e0\u05d9, \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d7\u05d9\u05d1\u05d5\u05e8 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05dc\u05d5\u05dc \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05d2\u05d9\u05e9\u05d4 \u05dc\u05de\u05de\u05e9\u05e7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05d7\u05d5\u05e5 \u05dc-Home Assistant \u05d1\u05d6\u05de\u05df \u05e9\u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e4\u05e2\u05d9\u05dc, \u05d5\u05dc\u05d4\u05d9\u05e4\u05da." } } } diff --git a/homeassistant/components/hue/translations/he.json b/homeassistant/components/hue/translations/he.json index 3b03fb84bc3..1621e947d68 100644 --- a/homeassistant/components/hue/translations/he.json +++ b/homeassistant/components/hue/translations/he.json @@ -22,7 +22,18 @@ "link": { "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05e2\u05dc \u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d3\u05d9 \u05dc\u05d7\u05d1\u05e8 \u05d1\u05d9\u05df \u05d0\u05ea Philips Hue \u05e2\u05dd Home Assistant. \n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05e8\u05db\u05d6\u05ea](/static/images/config_philips_hue.jpg)", "title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05e8\u05db\u05d6\u05ea" + }, + "manual": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } } } + }, + "device_automation": { + "trigger_subtype": { + "turn_off": "\u05db\u05d1\u05d4", + "turn_on": "\u05d4\u05e4\u05e2\u05dc" + } } } \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/he.json b/homeassistant/components/huisbaasje/translations/he.json new file mode 100644 index 00000000000..d1411d63b8d --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/he.json b/homeassistant/components/humidifier/translations/he.json new file mode 100644 index 00000000000..aa14b7d234f --- /dev/null +++ b/homeassistant/components/humidifier/translations/he.json @@ -0,0 +1,25 @@ +{ + "device_automation": { + "action_type": { + "set_mode": "\u05e9\u05e0\u05d4 \u05de\u05e6\u05d1 \u05d1-{entity_name}", + "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}", + "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} \u05de\u05d5\u05d2\u05d3\u05e8 \u05dc\u05de\u05e6\u05d1 \u05de\u05e1\u05d5\u05d9\u05dd", + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + }, + "trigger_type": { + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" + } + }, + "state": { + "_": { + "off": "\u05db\u05d1\u05d5\u05d9", + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/de.json b/homeassistant/components/hunterdouglas_powerview/translations/de.json index 4c843fb262f..db0fa18cc29 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/de.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/de.json @@ -7,6 +7,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "M\u00f6chten Sie {name} ({host}) einrichten?", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json index 39beabfa06e..87b0a5d8f50 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/he.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} ({host})", "step": { + "link": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + }, "user": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" diff --git a/homeassistant/components/hvv_departures/translations/he.json b/homeassistant/components/hvv_departures/translations/he.json index ac90b3264ea..bb4fc738217 100644 --- a/homeassistant/components/hvv_departures/translations/he.json +++ b/homeassistant/components/hvv_departures/translations/he.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/hyperion/translations/he.json b/homeassistant/components/hyperion/translations/he.json new file mode 100644 index 00000000000..7dafc7565e5 --- /dev/null +++ b/homeassistant/components/hyperion/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea Hyperion Ambilight \u05d4\u05d6\u05d4 \u05dc-Home Assistant?\n\n**\u05de\u05d0\u05e8\u05d7:** {host}\n**\u05e4\u05ea\u05d7\u05d4:** {port}\n**\u05de\u05d6\u05d4\u05d4**: {id}" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/he.json b/homeassistant/components/ialarm/translations/he.json new file mode 100644 index 00000000000..b2a3b1b660e --- /dev/null +++ b/homeassistant/components/ialarm/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/he.json b/homeassistant/components/iaqualink/translations/he.json index 6f4191da70d..2ec9b1b240b 100644 --- a/homeassistant/components/iaqualink/translations/he.json +++ b/homeassistant/components/iaqualink/translations/he.json @@ -3,8 +3,10 @@ "step": { "user": { "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df iAqualink \u05e9\u05dc\u05da." } } } diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index 139d7a1e399..3320e8f3dcc 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -1,11 +1,38 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "step": { + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d6\u05e0\u05ea \u05d1\u05e2\u05d1\u05e8 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05e4\u05d5\u05e2\u05dc\u05ea \u05e2\u05d5\u05d3. \u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4.", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "trusted_device": { + "data": { + "trusted_device": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df" + }, + "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da", + "title": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9" - } + "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9", + "with_family": "\u05e2\u05dd \u05d4\u05de\u05e9\u05e4\u05d7\u05d4" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9\u05dd \u05e9\u05dc\u05da", + "title": "\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05d9\u05e0\u05d5 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05e7\u05d9\u05d1\u05dc\u05ea\u05dd \u05d6\u05d4 \u05e2\u05ea\u05d4 \u05de-iCloud", + "title": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea iCloud" } } } diff --git a/homeassistant/components/insteon/translations/he.json b/homeassistant/components/insteon/translations/he.json index 8aabed0bfce..220bbcbb632 100644 --- a/homeassistant/components/insteon/translations/he.json +++ b/homeassistant/components/insteon/translations/he.json @@ -1,9 +1,47 @@ { - "options": { + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { + "hubv1": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, + "hubv2": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "plm": { + "data": { + "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + } + } + }, + "options": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "add_x10": { + "description": "\u05e9\u05e0\u05d4 \u05d0\u05ea \u05e1\u05d9\u05e1\u05de\u05ea \u05e8\u05db\u05d6\u05ea Insteon." + }, "change_hub_config": { "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/ios/translations/he.json b/homeassistant/components/ios/translations/he.json index deb8eae6b38..a18f311e43a 100644 --- a/homeassistant/components/ios/translations/he.json +++ b/homeassistant/components/ios/translations/he.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc Home Assistant iOS \u05e0\u05d7\u05d5\u05e6\u05d4." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" } } } diff --git a/homeassistant/components/ipp/translations/he.json b/homeassistant/components/ipp/translations/he.json new file mode 100644 index 00000000000..7acc23c09e9 --- /dev/null +++ b/homeassistant/components/ipp/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/he.json b/homeassistant/components/iqvia/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/iqvia/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 00397ca5dd8..cf658384184 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -39,7 +39,10 @@ }, "system_health": { "info": { - "host_reachable": "Deutsch" + "device_connected": "ISY verbunden", + "host_reachable": "Deutsch", + "last_heartbeat": "Letzte Heartbeat-Zeit", + "websocket_status": "Ereignis-Socket Status" } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json index ed6f54fc696..2485285743f 100644 --- a/homeassistant/components/isy994/translations/he.json +++ b/homeassistant/components/isy994/translations/he.json @@ -5,17 +5,25 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_host": "\u05e2\u05e8\u05da \u05d4\u05beHost \u05dc\u05d0 \u05d4\u05d9\u05d4 \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "description": "\u05d4\u05e2\u05e8\u05da \u05d4\u05de\u05d0\u05e8\u05d7 \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80" } } + }, + "system_health": { + "info": { + "host_reachable": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05d2\u05d9\u05e2 \u05dc\u05de\u05d0\u05e8\u05d7" + } } } \ No newline at end of file diff --git a/homeassistant/components/izone/translations/he.json b/homeassistant/components/izone/translations/he.json new file mode 100644 index 00000000000..380dbc5d7fc --- /dev/null +++ b/homeassistant/components/izone/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/he.json b/homeassistant/components/juicenet/translations/he.json index 863e2560cbd..384ea203a51 100644 --- a/homeassistant/components/juicenet/translations/he.json +++ b/homeassistant/components/juicenet/translations/he.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" }, "step": { "user": { + "data": { + "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + }, "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05beAPI \u05de\u05behttps://home.juice.net/Manage." } } diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json index 68de4001255..c165cbd9043 100644 --- a/homeassistant/components/keenetic_ndms2/translations/de.json +++ b/homeassistant/components/keenetic_ndms2/translations/de.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "no_udn": "SSDP-Erkennungsinfo hat keine UDN", + "not_keenetic_ndms2": "Gefundenes Ger\u00e4t ist kein Keenetic-Router" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -13,7 +16,8 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "title": "Keenetic NDMS2 Router einrichten" } } }, @@ -21,8 +25,12 @@ "step": { "user": { "data": { + "consider_home": "Heimintervall ber\u00fccksichtigen", + "include_arp": "ARP-Daten verwenden (wird ignoriert, wenn Hotspot-Daten verwendet werden)", + "include_associated": "WLAN-AP-Zuordnungsdaten verwenden (wird ignoriert, wenn Hotspot-Daten verwendet werden)", "interfaces": "Schnittstellen zum Scannen ausw\u00e4hlen", - "scan_interval": "Scanintervall" + "scan_interval": "Scanintervall", + "try_hotspot": "'ip hotspot'-Daten verwenden (am genauesten)" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/he.json b/homeassistant/components/keenetic_ndms2/translations/he.json new file mode 100644 index 00000000000..fd25964fcd0 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/he.json b/homeassistant/components/kmtronic/translations/he.json new file mode 100644 index 00000000000..7b48912f1b4 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 5f2badfd78d..6a137ac4350 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -22,7 +22,8 @@ "description": "Bitte gib deinen Kodi-Benutzernamen und Passwort ein. Diese findest du unter System/Einstellungen/Netzwerk/Dienste." }, "discovery_confirm": { - "description": "M\u00f6chtest du Kodi (` {name} `) zu Home Assistant hinzuf\u00fcgen?" + "description": "M\u00f6chtest du Kodi (` {name} `) zu Home Assistant hinzuf\u00fcgen?", + "title": "Gefundene Kodi-Installation" }, "user": { "data": { @@ -35,8 +36,15 @@ "ws_port": { "data": { "ws_port": "Port" - } + }, + "description": "Der WebSocket-Port (in Kodi manchmal TCP-Port genannt). Um eine Verbindung \u00fcber WebSocket herzustellen, m\u00fcssen Sie unter System/Einstellungen/Netzwerk/Dienste \"Programme ... zur Steuerung von Kodi zulassen\" aktivieren. Wenn WebSocket nicht aktiviert ist, entfernen Sie den Port und lassen ihn leer." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} wurde zum Ausschalten aufgefordert", + "turn_on": "{entity_name} wurde zum Einschalten aufgefordert" + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json new file mode 100644 index 00000000000..89b6a0c30b9 --- /dev/null +++ b/homeassistant/components/kodi/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "credentials": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05d1-Kodi. \u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05dd \u05d1\u05de\u05e2\u05e8\u05db\u05ea/\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea/\u05e8\u05e9\u05ea/\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9\u05dd." + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 7938f1a68bd..69d483e92c9 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -87,6 +87,7 @@ "data": { "api_host": "API-Host-URL \u00fcberschreiben (optional)", "blink": "LED Panel blinkt beim senden von Status\u00e4nderungen", + "discovery": "Reagieren auf Suchanfragen in Ihrem Netzwerk", "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" }, "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel", diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json new file mode 100644 index 00000000000..c31f537e698 --- /dev/null +++ b/homeassistant/components/konnected/translations/he.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "confirm": { + "description": "\u05d3\u05d2\u05dd: {model}\n\u05de\u05d6\u05d4\u05d4: {id}\n\u05de\u05d0\u05e8\u05d7: {host}\n\u05e4\u05ea\u05d7\u05d4: {port}\n\n\u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 \u05e9\u05dc \u05e4\u05ea\u05d9\u05d7\u05d4 \u05d5\u05e1\u05d2\u05d9\u05e8\u05d4 \u05d5\u05e9\u05dc \u05d4\u05d7\u05dc\u05d5\u05e0\u05d9\u05ea \u05d1\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05dc\u05d5\u05d7 \u05d4\u05d0\u05d6\u05e2\u05e7\u05d4 \u05e9\u05dc Konnected." + }, + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, + "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05de\u05d0\u05e8\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d5\u05d7 Konnected \u05e9\u05dc\u05da." + } + } + }, + "options": { + "step": { + "options_io": { + "description": "\u05d4\u05ea\u05d2\u05dc\u05d4 {model} \u05d1-{host} . \u05d1\u05d7\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d1\u05e1\u05d9\u05e1 \u05e9\u05dc \u05db\u05dc \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05dc\u05de\u05d8\u05d4 - \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d6\u05d4 \u05e2\u05e9\u05d5\u05d9 \u05dc\u05d0\u05e4\u05e9\u05e8 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd (\u05de\u05d2\u05e2\u05d9\u05dd \u05e4\u05ea\u05d5\u05d7\u05d9\u05dd/\u05e1\u05d2\u05d5\u05e8\u05d9\u05dd), \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d3\u05d9\u05d2\u05d9\u05d8\u05dc\u05d9\u05d9\u05dd (dht \u05d5-ds18b20), \u05d0\u05d5 \u05d9\u05e6\u05d9\u05d0\u05d5\u05ea \u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d7\u05dc\u05e4\u05d4. \u05ea\u05d5\u05db\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05e4\u05d5\u05e8\u05d8\u05d5\u05ea \u05d1\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05d1\u05d0\u05d9\u05dd." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index da88fb0ac4d..6b41217dca4 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -32,7 +32,9 @@ "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto" }, "error": { - "bad_host": "URL host API di sostituzione non valido" + "bad_host": "URL host API di sostituzione non valido", + "one": "Pi\u00f9", + "other": "Altri" }, "step": { "options_binary": { diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json index 095487fff3f..dfd568f937c 100644 --- a/homeassistant/components/kostal_plenticore/translations/de.json +++ b/homeassistant/components/kostal_plenticore/translations/de.json @@ -16,5 +16,6 @@ } } } - } + }, + "title": "Kostal Plenticore Solar-Wechselrichter" } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/he.json b/homeassistant/components/kostal_plenticore/translations/he.json new file mode 100644 index 00000000000..56476cccb7c --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/de.json b/homeassistant/components/kraken/translations/de.json index 9a5d52f939e..d0a845edfd4 100644 --- a/homeassistant/components/kraken/translations/de.json +++ b/homeassistant/components/kraken/translations/de.json @@ -13,6 +13,7 @@ "step": { "init": { "data": { + "scan_interval": "Update-Intervall", "tracked_asset_pairs": "Verfolgte Asset-Paare" } } diff --git a/homeassistant/components/kraken/translations/he.json b/homeassistant/components/kraken/translations/he.json new file mode 100644 index 00000000000..4676729e600 --- /dev/null +++ b/homeassistant/components/kraken/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/it.json b/homeassistant/components/kraken/translations/it.json index a436844fb75..4b646be7a8d 100644 --- a/homeassistant/components/kraken/translations/it.json +++ b/homeassistant/components/kraken/translations/it.json @@ -3,8 +3,16 @@ "abort": { "already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "one": "Pi\u00f9", + "other": "Altri" + }, "step": { "user": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + }, "description": "Vuoi iniziare la configurazione?" } } diff --git a/homeassistant/components/kulersky/translations/he.json b/homeassistant/components/kulersky/translations/he.json new file mode 100644 index 00000000000..d3d68dccc93 --- /dev/null +++ b/homeassistant/components/kulersky/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json index 3007c0e968c..e6fa6cd1db9 100644 --- a/homeassistant/components/life360/translations/he.json +++ b/homeassistant/components/life360/translations/he.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json new file mode 100644 index 00000000000..380dbc5d7fc --- /dev/null +++ b/homeassistant/components/lifx/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/he.json b/homeassistant/components/light/translations/he.json index bb49ba266a7..9d4b3633088 100644 --- a/homeassistant/components/light/translations/he.json +++ b/homeassistant/components/light/translations/he.json @@ -1,4 +1,15 @@ { + "device_automation": { + "action_type": { + "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}", + "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/litejet/translations/he.json b/homeassistant/components/litejet/translations/he.json index a06c89f1d2a..406e020905e 100644 --- a/homeassistant/components/litejet/translations/he.json +++ b/homeassistant/components/litejet/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/litterrobot/translations/he.json b/homeassistant/components/litterrobot/translations/he.json new file mode 100644 index 00000000000..454b7e1ae51 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/he.json b/homeassistant/components/local_ip/translations/he.json new file mode 100644 index 00000000000..08506bf3437 --- /dev/null +++ b/homeassistant/components/local_ip/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/he.json b/homeassistant/components/logi_circle/translations/he.json new file mode 100644 index 00000000000..425a68fee00 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "error": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/he.json b/homeassistant/components/lovelace/translations/he.json new file mode 100644 index 00000000000..45098b52165 --- /dev/null +++ b/homeassistant/components/lovelace/translations/he.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "dashboards": "\u05dc\u05d5\u05d7\u05d5\u05ea \u05d1\u05e7\u05e8\u05d4", + "mode": "\u05de\u05e6\u05d1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 1950edd8ff2..49cd36628ee 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -15,6 +15,7 @@ "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen." }, "link": { + "description": "Um ein Pairing mit {name} ({host}) durchzuf\u00fchren, dr\u00fccken Sie nach dem Absenden dieses Formulars die schwarze Taste auf der R\u00fcckseite der Br\u00fccke.", "title": "Mit der Bridge verbinden" }, "user": { @@ -32,6 +33,20 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "close_1": "Einen schlie\u00dfen", + "close_2": "Zwei schlie\u00dfen", + "close_3": "Drei schlie\u00dfen", + "close_4": "Vier schlie\u00dfen", + "close_all": "Alle schlie\u00dfen", + "group_1_button_1": "Erste Gruppe erste Taste", + "group_1_button_2": "Erste Gruppe zweite Taste", + "group_2_button_1": "Zweite Gruppe erste Taste", + "group_2_button_2": "Zweite Gruppe zweite Taste", + "lower": "Unter", + "lower_1": "Unterer", + "lower_2": "Untere zwei", + "lower_3": "Untere drei", + "lower_4": "Untere vier", "lower_all": "Alle senken", "off": "Aus", "on": "An", diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json index 7b55b0743fb..163a150d00c 100644 --- a/homeassistant/components/lutron_caseta/translations/he.json +++ b/homeassistant/components/lutron_caseta/translations/he.json @@ -1,7 +1,17 @@ { "config": { + "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d2\u05e9\u05e8 (\u05de\u05d0\u05e8\u05d7: {host}) \u05d4\u05de\u05d9\u05d5\u05d1\u05d0 \u05de-configuration.yaml." + }, + "link": { + "description": "\u05db\u05d3\u05d9 \u05dc\u05d6\u05d5\u05d5\u05d2 \u05e2\u05dd {name} ({host}), \u05dc\u05d0\u05d7\u05e8 \u05e9\u05dc\u05d9\u05d7\u05ea \u05d8\u05d5\u05e4\u05e1 \u05d6\u05d4, \u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05dc\u05d7\u05e6\u05df \u05d4\u05e9\u05d7\u05d5\u05e8 \u05d1\u05d2\u05d1 \u05d4\u05d2\u05e9\u05e8." + }, "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8." } } diff --git a/homeassistant/components/lyric/translations/he.json b/homeassistant/components/lyric/translations/he.json new file mode 100644 index 00000000000..be83e5f2fed --- /dev/null +++ b/homeassistant/components/lyric/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/he.json b/homeassistant/components/mazda/translations/he.json new file mode 100644 index 00000000000..9856fb9034c --- /dev/null +++ b/homeassistant/components/mazda/translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "account_locked": "\u05d7\u05e9\u05d1\u05d5\u05df \u05e0\u05e2\u05d5\u05dc. \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1 \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d0\u05ea\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/he.json b/homeassistant/components/media_player/translations/he.json index f29b3add414..8efa77488ba 100644 --- a/homeassistant/components/media_player/translations/he.json +++ b/homeassistant/components/media_player/translations/he.json @@ -1,9 +1,25 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u05de\u05de\u05ea\u05d9\u05df", + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc", + "is_paused": "{entity_name} \u05de\u05d5\u05e9\u05d4\u05d4", + "is_playing": "{entity_name} \u05de\u05ea\u05e0\u05d2\u05df" + }, + "trigger_type": { + "idle": "{entity_name} \u05d4\u05d5\u05e4\u05da \u05dc\u05de\u05de\u05ea\u05d9\u05df", + "paused": "{entity_name} \u05de\u05d5\u05e9\u05d4\u05d4", + "playing": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05d2\u05df", + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" + } + }, "state": { "_": { "idle": "\u05de\u05de\u05ea\u05d9\u05df", "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7", + "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "paused": "\u05de\u05d5\u05e9\u05d4\u05d4", "playing": "\u05de\u05e0\u05d2\u05df", "standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4" diff --git a/homeassistant/components/melcloud/translations/he.json b/homeassistant/components/melcloud/translations/he.json index ac90b3264ea..d04ae43734b 100644 --- a/homeassistant/components/melcloud/translations/he.json +++ b/homeassistant/components/melcloud/translations/he.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "username": "\u05d3\u05d5\u05d0\"\u05dc" } } } diff --git a/homeassistant/components/met/translations/de.json b/homeassistant/components/met/translations/de.json index e2bb171c749..0bc30508ab8 100644 --- a/homeassistant/components/met/translations/de.json +++ b/homeassistant/components/met/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "In der Konfiguration von Home Assistant sind keine Home-Koordinaten eingestellt" + }, "error": { "already_configured": "Der Dienst ist bereits konfiguriert" }, diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json index 0d979ed800b..a7e1ff9668b 100644 --- a/homeassistant/components/met_eireann/translations/de.json +++ b/homeassistant/components/met_eireann/translations/de.json @@ -11,6 +11,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, + "description": "Geben Sie Ihren Standort ein, um Wetterdaten von der Met \u00c9ireann Public Weather Forecast API zu verwenden", "title": "Standort" } } diff --git a/homeassistant/components/meteo_france/translations/he.json b/homeassistant/components/meteo_france/translations/he.json new file mode 100644 index 00000000000..7055f451412 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "cities": { + "data": { + "city": "\u05e2\u05d9\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/de.json b/homeassistant/components/meteoclimatic/translations/de.json new file mode 100644 index 00000000000..e23662146b2 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "not_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "step": { + "user": { + "data": { + "code": "Stationscode" + }, + "description": "Geben Sie den Code der Meteoclimatic-Station ein (z. B. ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/he.json b/homeassistant/components/meteoclimatic/translations/he.json new file mode 100644 index 00000000000..db961a2f14c --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "step": { + "user": { + "data": { + "code": "\u05e7\u05d5\u05d3 \u05ea\u05d7\u05e0\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/he.json b/homeassistant/components/metoffice/translations/he.json index 4c49313d977..eb9c1a2a7ca 100644 --- a/homeassistant/components/metoffice/translations/he.json +++ b/homeassistant/components/metoffice/translations/he.json @@ -3,6 +3,8 @@ "step": { "user": { "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" } } diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json index ac90b3264ea..c4578e1c322 100644 --- a/homeassistant/components/mikrotik/translations/he.json +++ b/homeassistant/components/mikrotik/translations/he.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/mill/translations/he.json b/homeassistant/components/mill/translations/he.json new file mode 100644 index 00000000000..40170220ad7 --- /dev/null +++ b/homeassistant/components/mill/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/he.json b/homeassistant/components/minecraft_server/translations/he.json new file mode 100644 index 00000000000..4240227d571 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05e9\u05e8\u05ea \u05e0\u05db\u05e9\u05dc\u05d4. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4 \u05d5\u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05db\u05de\u05d5 \u05db\u05df, \u05d5\u05d3\u05d0 \u05e9\u05d0\u05ea\u05d4 \u05de\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea Minecraft \u05d2\u05d9\u05e8\u05e1\u05d4 1.7 \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05e9\u05e8\u05ea \u05e9\u05dc\u05da." + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/he.json b/homeassistant/components/monoprice/translations/he.json new file mode 100644 index 00000000000..80169dd26e7 --- /dev/null +++ b/homeassistant/components/monoprice/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.he.json b/homeassistant/components/moon/translations/sensor.he.json new file mode 100644 index 00000000000..918f8fec5f9 --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.he.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05e8\u05d0\u05e9\u05d5\u05df", + "full_moon": "\u05d9\u05e8\u05d7 \u05de\u05dc\u05d0", + "last_quarter": "\u05e8\u05d1\u05e2\u05d5\u05df \u05d0\u05d7\u05e8\u05d5\u05df", + "new_moon": "\u05d9\u05e8\u05d7 \u05d7\u05d3\u05e9", + "waning_crescent": "\u05e1\u05d4\u05e8 \u05d3\u05d5\u05e2\u05da", + "waning_gibbous": "\u05d2\u05d1\u05e2\u05d5\u05dc\u05d9 \u05e0\u05d5\u05d3\u05d3", + "waxing_crescent": "\u05d7\u05e6\u05d9 \u05e1\u05d4\u05e8 \u05e9\u05e2\u05d5\u05d5\u05d4", + "waxing_gibbous": "\u05e1\u05d9\u05d1\u05d9\u05ea \u05e9\u05e2\u05d5\u05d5\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json new file mode 100644 index 00000000000..0876de6504a --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/he.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "connect": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, + "select": { + "data": { + "select_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json index 94b86f04b2d..3370717366d 100644 --- a/homeassistant/components/motioneye/translations/de.json +++ b/homeassistant/components/motioneye/translations/de.json @@ -11,6 +11,10 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "hassio_confirm": { + "description": "M\u00f6chten Sie Home Assistant so konfigurieren, dass er sich mit dem motionEye-Dienst des Add-ons {addon} verbindet?", + "title": "motionEye \u00fcber Home Assistant Add-on" + }, "user": { "data": { "admin_password": "Admin Passwort", diff --git a/homeassistant/components/motioneye/translations/he.json b/homeassistant/components/motioneye/translations/he.json new file mode 100644 index 00000000000..0397abd99b8 --- /dev/null +++ b/homeassistant/components/motioneye/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "admin_password": "\u05e1\u05d9\u05e1\u05de\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05de\u05e2\u05e8\u05db\u05ea", + "admin_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05de\u05e0\u05d4\u05dc \u05de\u05e2\u05e8\u05db\u05ea", + "surveillance_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05e9\u05d2\u05d9\u05d7", + "surveillance_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05de\u05e9\u05d2\u05d9\u05d7", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index ffd2d1b36a1..132b4c42e18 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -62,13 +62,22 @@ "port": "Port", "username": "Benutzername" }, - "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein." + "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", + "title": "Broker-Optionen" }, "options": { "data": { + "birth_enable": "Birth Nachricht aktivieren", "birth_payload": "Nutzdaten der Birth Nachricht", + "birth_qos": "Birth Nachricht QoS", + "birth_retain": "Birth Nachricht zwischenspeichern", + "birth_topic": "Thema der Birth Nachricht", "discovery": "Erkennung aktivieren", - "will_enable": "Letzten Willen aktivieren" + "will_enable": "Letzten Willen aktivieren", + "will_payload": "Nutzdaten der Letzter-Wille Nachricht", + "will_qos": "Letzter-Wille Nachricht QoS", + "will_retain": "Letzter-Wille Nachricht zwischenspeichern", + "will_topic": "Thema der Letzter-Wille Nachricht" }, "description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Entit\u00e4ten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nGeburtsnachricht - Die Geburtsnachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z. B. Herunterfahren von Home Assistant) als auch im Falle einer unsauberen (z. B. Absturz von Home Assistant oder Verlust der Netzwerkverbindung) Verbindungstrennung.", "title": "MQTT-Optionen" diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index bd083dfc1ec..0b809b8d18e 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "\u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d0\u05d7\u05ea \u05e9\u05dc MQTT \u05de\u05d5\u05ea\u05e8\u05ea." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { - "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d1\u05e8\u05d5\u05e7\u05e8." + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "broker": { @@ -12,11 +12,25 @@ "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", "discovery": "\u05d0\u05e4\u05e9\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "port": "\u05e4\u05d5\u05e8\u05d8", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da." } } + }, + "options": { + "step": { + "broker": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da" + }, + "options": { + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/he.json b/homeassistant/components/mutesync/translations/he.json new file mode 100644 index 00000000000..00011f86933 --- /dev/null +++ b/homeassistant/components/mutesync/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/he.json b/homeassistant/components/myq/translations/he.json index ac90b3264ea..76815ee91d3 100644 --- a/homeassistant/components/myq/translations/he.json +++ b/homeassistant/components/myq/translations/he.json @@ -1,6 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05e2\u05d5\u05d3." + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index c61c4177136..bb6a1ed7bfe 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -15,24 +15,37 @@ "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", "not_a_number": "Bitte eine Nummer eingeben", + "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein", + "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich", "unknown": "Unerwarteter Fehler" }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", + "duplicate_persistence_file": "Persistenzdatei wird bereits verwendet", + "duplicate_topic": "Thema bereits in Verwendung", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_device": "Ung\u00fcltiges Ger\u00e4t", "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_persistence_file": "Ung\u00fcltige Persistenzdatei", + "invalid_port": "Ung\u00fcltige Portnummer", + "invalid_publish_topic": "Ung\u00fcltiges Ver\u00f6ffentlichungsthema", "invalid_serial": "Ung\u00fcltiger Serieller Port", + "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", + "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein", + "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich", "unknown": "Unerwarteter Fehler" }, "step": { "gw_mqtt": { "data": { + "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)", "retain": "MQTT behalten", + "topic_in_prefix": "Pr\u00e4fix f\u00fcr Eingabethemen (topic_in_prefix)", + "topic_out_prefix": "Pr\u00e4fix f\u00fcr Ausgabethemen (topic_out_prefix)", "version": "MySensors Version" }, "description": "MQTT-Gateway einrichten" @@ -41,6 +54,7 @@ "data": { "baud_rate": "Baudrate", "device": "Serielle Schnittstelle", + "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)", "version": "MySensors Version" }, "description": "Einrichtung des seriellen Gateways" @@ -48,6 +62,7 @@ "gw_tcp": { "data": { "device": "IP-Adresse des Gateways", + "persistence_file": "Persistenzdatei (leer lassen, um automatisch zu generieren)", "tcp_port": "Port", "version": "MySensors Version" }, diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json new file mode 100644 index 00000000000..e9366a4e2ef --- /dev/null +++ b/homeassistant/components/mysensors/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index 823a9675726..e3c7159a0d6 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -16,7 +16,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Einrichten der Nettigo Air Monitor Integration." } } } diff --git a/homeassistant/components/nam/translations/he.json b/homeassistant/components/nam/translations/he.json new file mode 100644 index 00000000000..39f3a7e4306 --- /dev/null +++ b/homeassistant/components/nam/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Nettigo Air Monitor \u05d1-{host}?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json new file mode 100644 index 00000000000..876a9eded8b --- /dev/null +++ b/homeassistant/components/neato/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 6f43df5ac81..8b44c715ede 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,10 +1,17 @@ { "config": { "abort": { - "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea" + "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" }, "error": { "internal_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05e4\u05e0\u05d9\u05de\u05d9\u05ea \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3", + "invalid_pin": "\u05e7\u05d5\u05d3 PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3" }, @@ -13,15 +20,22 @@ "data": { "flow_impl": "\u05e1\u05e4\u05e7" }, - "description": "\u05d1\u05d7\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05e4\u05e7 \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d0\u05de\u05ea \u05e2\u05dd Nest.", + "description": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea", "title": "\u05e1\u05e4\u05e7 \u05d0\u05d9\u05de\u05d5\u05ea" }, "link": { "data": { - "code": "\u05e7\u05d5\u05d3 Pin" + "code": "\u05e7\u05d5\u05d3 PIN" }, "description": "\u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05d0\u05ea \u05d7\u05e9\u05d1\u05d5\u05df Nest \u05e9\u05dc\u05da, [\u05d0\u05de\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da] ({url}). \n\n \u05dc\u05d0\u05d7\u05e8 \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8, \u05d4\u05e2\u05ea\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4PIN \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d5\u05d4\u05d3\u05d1\u05e7 \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4.", "title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df Nest" + }, + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 Nest \u05e6\u05e8\u05d9\u05da \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 1037d100909..73106797381 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -49,6 +49,7 @@ "mode": "Berechnung", "show_on_map": "Auf Karte anzeigen" }, + "description": "Konfigurieren Sie einen \u00f6ffentlichen Wettersensor f\u00fcr einen Bereich.", "title": "\u00d6ffentlicher Netatmo Wettersensor" }, "public_weather_areas": { diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json index 54bef84c30a..85fd7994cf7 100644 --- a/homeassistant/components/netatmo/translations/he.json +++ b/homeassistant/components/netatmo/translations/he.json @@ -1,11 +1,27 @@ { + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + }, "device_automation": { + "trigger_subtype": { + "away": "\u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea" + }, "trigger_type": { - "animal": "\u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc-\u05d7\u05d9\u05d9\u05dd", - "human": "\u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd", - "movement": "\u05d6\u05d9\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", - "turned_off": "\u05db\u05d1\u05d4", - "turned_on": "\u05e0\u05d3\u05dc\u05e7" + "animal": "{entity_name} \u05d6\u05d9\u05d4\u05d4 \u05d1\u05e2\u05dc \u05d7\u05d9\u05d9\u05dd", + "human": "{entity_name} \u05d6\u05d9\u05d4\u05d4 \u05d0\u05d3\u05dd", + "movement": "\u05ea\u05e0\u05d5\u05e2\u05d4 \u05d6\u05d5\u05d4\u05ea\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 {entity_name}", + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } } } \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json index f2220f828e8..e2a9a9bc739 100644 --- a/homeassistant/components/nexia/translations/de.json +++ b/homeassistant/components/nexia/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marke", "password": "Passwort", "username": "Benutzername" }, diff --git a/homeassistant/components/nexia/translations/he.json b/homeassistant/components/nexia/translations/he.json index ac90b3264ea..7f9efa5b24e 100644 --- a/homeassistant/components/nexia/translations/he.json +++ b/homeassistant/components/nexia/translations/he.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "brand": "\u05de\u05d5\u05ea\u05d2", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc-mynexia.com" } } } diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 510d57ce45f..91461416c60 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -14,7 +14,9 @@ "data": { "api_key": "API-Schl\u00fcssel", "url": "URL" - } + }, + "description": "- URL: die Adresse Ihrer Nightscout-Instanz. Z.B.: https://myhomeassistant.duckdns.org:5423\n- API-Schl\u00fcssel (Optional): Nur verwenden, wenn Ihre Instanz gesch\u00fctzt ist (auth_default_roles != readable).", + "title": "Geben Sie Ihre Nightscout-Serverinformationen ein." } } } diff --git a/homeassistant/components/nightscout/translations/he.json b/homeassistant/components/nightscout/translations/he.json new file mode 100644 index 00000000000..c39016d3bb6 --- /dev/null +++ b/homeassistant/components/nightscout/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json index 3007c0e968c..837c2ac983b 100644 --- a/homeassistant/components/notion/translations/he.json +++ b/homeassistant/components/notion/translations/he.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/nuheat/translations/he.json b/homeassistant/components/nuheat/translations/he.json index ac90b3264ea..c479d8488f2 100644 --- a/homeassistant/components/nuheat/translations/he.json +++ b/homeassistant/components/nuheat/translations/he.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index ae1322d7641..2631c7b0588 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -13,6 +13,7 @@ "data": { "token": "Zugangstoken" }, + "description": "Die Nuki-Integration muss sich bei deiner Bridge neu authentifizieren.", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/nuki/translations/he.json b/homeassistant/components/nuki/translations/he.json new file mode 100644 index 00000000000..971fe867c28 --- /dev/null +++ b/homeassistant/components/nuki/translations/he.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/he.json b/homeassistant/components/number/translations/he.json new file mode 100644 index 00000000000..b71257ef066 --- /dev/null +++ b/homeassistant/components/number/translations/he.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "\u05d4\u05d2\u05d3\u05e8 \u05e2\u05e8\u05da \u05e2\u05d1\u05d5\u05e8 {entity_name}" + } + }, + "title": "\u05de\u05e1\u05e4\u05e8" +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json index ac90b3264ea..b3fb785d55e 100644 --- a/homeassistant/components/nut/translations/he.json +++ b/homeassistant/components/nut/translations/he.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/nzbget/translations/he.json b/homeassistant/components/nzbget/translations/he.json new file mode 100644 index 00000000000..bb4fc738217 --- /dev/null +++ b/homeassistant/components/nzbget/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/he.json b/homeassistant/components/omnilogic/translations/he.json new file mode 100644 index 00000000000..b38e5b12c8b --- /dev/null +++ b/homeassistant/components/omnilogic/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/translations/he.json b/homeassistant/components/onboarding/translations/he.json new file mode 100644 index 00000000000..683b005376f --- /dev/null +++ b/homeassistant/components/onboarding/translations/he.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u05d7\u05d3\u05e8 \u05e9\u05d9\u05e0\u05d4", + "kitchen": "\u05de\u05d8\u05d1\u05d7", + "living_room": "\u05e1\u05dc\u05d5\u05df" + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/he.json b/homeassistant/components/onewire/translations/he.json new file mode 100644 index 00000000000..7f3951315a9 --- /dev/null +++ b/homeassistant/components/onewire/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "owserver": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index ecfa1afaab2..b0ea84c04ba 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, "step": { "auth": { "data": { @@ -12,9 +16,15 @@ "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" } }, + "device": { + "data": { + "host": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4" + } + }, "manual_input": { "data": { - "host": "Host", + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", "port": "\u05e4\u05d5\u05e8\u05d8" } } diff --git a/homeassistant/components/opentherm_gw/translations/he.json b/homeassistant/components/opentherm_gw/translations/he.json new file mode 100644 index 00000000000..2e4c2bd6cc2 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/he.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "init": { + "data": { + "device": "\u05e0\u05ea\u05d9\u05d1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "id": "\u05de\u05d6\u05d4\u05d4", + "name": "\u05e9\u05dd" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "temporary_override_mode": "\u05de\u05e6\u05d1 \u05e2\u05e7\u05d9\u05e4\u05d4 \u05d6\u05de\u05e0\u05d9 \u05e9\u05dc \u05e0\u05e7\u05d5\u05d3\u05ea \u05e2\u05e8\u05db\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/he.json b/homeassistant/components/openuv/translations/he.json index 1a93fb4b438..af4ec56b10c 100644 --- a/homeassistant/components/openuv/translations/he.json +++ b/homeassistant/components/openuv/translations/he.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API \u05e9\u05dc OpenUV", - "elevation": "\u05d2\u05d5\u05d1\u05d4 \u05de\u05e2\u05dc \u05e4\u05e0\u05d9 \u05d4\u05d9\u05dd", + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "elevation": "\u05d2\u05d5\u05d1\u05d4", "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" }, diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json index 4c49313d977..920be9308a5 100644 --- a/homeassistant/components/openweathermap/translations/he.json +++ b/homeassistant/components/openweathermap/translations/he.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "language": "\u05e9\u05e4\u05d4", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "mode": "\u05de\u05e6\u05d1" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u05e9\u05e4\u05d4", + "mode": "\u05de\u05e6\u05d1" } } } diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json new file mode 100644 index 00000000000..7864218bc3b --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{username}", + "step": { + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "description": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d5\u05e4\u05e2 OVO \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d2\u05e9\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e9\u05dc\u05da." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 0bc533c0469..da313efbe6e 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." + "default": "Unter Android \u00f6ffnen Sie [die OwnTracks App]({android_url}), gehen Sie zu Einstellungen -> Verbindung. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffnen Sie [die OwnTracks App]({ios_url}), tippen Sie auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndern Sie die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen finden Sie in [der Dokumentation]({docs_url}).\n\n\u00dcbersetzt mit www.DeepL.com/Translator (kostenlose Version)" }, "step": { "user": { diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json new file mode 100644 index 00000000000..5d8373f0eb0 --- /dev/null +++ b/homeassistant/components/ozw/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "step": { + "on_supervisor": { + "data": { + "use_addon": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 '\u05de\u05e4\u05e7\u05d7 OpenZWave'" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 OpenZWave?", + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/he.json b/homeassistant/components/panasonic_viera/translations/he.json new file mode 100644 index 00000000000..f19da6f07e4 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "pairing": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json index 04648fe5845..e4ecd0d0b9d 100644 --- a/homeassistant/components/philips_js/translations/he.json +++ b/homeassistant/components/philips_js/translations/he.json @@ -1,7 +1,19 @@ { "config": { "error": { - "pairing_failure": "\u05e6\u05d9\u05de\u05d5\u05d3 \u05e0\u05db\u05e9\u05dc" + "pairing_failure": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e9\u05d9\u05d9\u05da: {error_id}" + }, + "step": { + "pair": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/he.json b/homeassistant/components/pi_hole/translations/he.json new file mode 100644 index 00000000000..9b4392617f9 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "api_key": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7", + "location": "\u05de\u05d9\u05e7\u05d5\u05dd", + "name": "\u05e9\u05dd", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/he.json b/homeassistant/components/picnic/translations/he.json new file mode 100644 index 00000000000..f668538909b --- /dev/null +++ b/homeassistant/components/picnic/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "country_code": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05de\u05d3\u05d9\u05e0\u05d4", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, + "title": "\u05e4\u05d9\u05e7\u05e0\u05d9\u05e7" +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index a95359abeaf..fc5718cacb2 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, "create_entry": { - "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + "default": "Ihr Plaato {device_type} mit dem Namen **{device_name}** wurde erfolgreich eingerichtet!" }, "error": { "invalid_webhook_device": "Du hast ein Ger\u00e4t gew\u00e4hlt, das das Senden von Daten an einen Webhook nicht unterst\u00fctzt. Es ist nur f\u00fcr die Airlock verf\u00fcgbar", @@ -19,7 +19,7 @@ "token": "F\u00fcgen Sie hier das Auth Token ein", "use_webhook": "Webhook verwenden" }, - "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{Ger\u00e4tetyp}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer", + "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{device_type}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer", "title": "API-Methode ausw\u00e4hlen" }, "user": { diff --git a/homeassistant/components/plaato/translations/he.json b/homeassistant/components/plaato/translations/he.json new file mode 100644 index 00000000000..937391f9327 --- /dev/null +++ b/homeassistant/components/plaato/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/he.json b/homeassistant/components/plex/translations/he.json new file mode 100644 index 00000000000..4c2a041ce41 --- /dev/null +++ b/homeassistant/components/plex/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "host_or_token": "\u05d7\u05d9\u05d9\u05d1 \u05dc\u05e1\u05e4\u05e7 \u05dc\u05e4\u05d7\u05d5\u05ea \u05d0\u05d7\u05d3 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df" + }, + "flow_title": "{name} ({host})", + "step": { + "manual_setup": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json new file mode 100644 index 00000000000..13cb7aea870 --- /dev/null +++ b/homeassistant/components/plugwise/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user_gateway": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05de\u05d6\u05d4\u05d4 Smile", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05d7\u05d9\u05d9\u05da \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/he.json b/homeassistant/components/plum_lightpad/translations/he.json new file mode 100644 index 00000000000..6018a28e06b --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/he.json b/homeassistant/components/point/translations/he.json new file mode 100644 index 00000000000..24decb09dd8 --- /dev/null +++ b/homeassistant/components/point/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "no_flows": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "error": { + "no_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?", + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/he.json b/homeassistant/components/poolsense/translations/he.json new file mode 100644 index 00000000000..f285e1ec479 --- /dev/null +++ b/homeassistant/components/poolsense/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index c9161526373..02ad906a568 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -17,6 +17,7 @@ "ip_address": "IP-Adresse", "password": "Passwort" }, + "description": "Das Kennwort ist in der Regel die letzten 5 Zeichen der Seriennummer des Backup Gateway und kann in der Tesla-App gefunden werden oder es sind die letzten 5 Zeichen des Kennworts, das sich in der T\u00fcr f\u00fcr Backup Gateway 2 befindet.", "title": "Stellen Sie eine Verbindung zur Powerwall her" } } diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json new file mode 100644 index 00000000000..7f772fab5f9 --- /dev/null +++ b/homeassistant/components/powerwall/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{ip_address}", + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/he.json b/homeassistant/components/progettihwsw/translations/he.json new file mode 100644 index 00000000000..67c80866be0 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/he.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Relay 1", + "relay_10": "Relay 10", + "relay_11": "Relay 11", + "relay_12": "Relay 12", + "relay_13": "Relay 13", + "relay_15": "Relay 15", + "relay_2": "Relay 2", + "relay_3": "Relay 3", + "relay_4": "Relay 4", + "relay_5": "Relay 5", + "relay_6": "Relay 6", + "relay_7": "Relay 7", + "relay_8": "Relay 8", + "relay_9": "Relay 9" + }, + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05de\u05e1\u05e8\u05d9\u05dd" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index ec0807982cc..837dd13b925 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -1,14 +1,19 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea." }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "creds": { "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" }, "link": { "data": { + "code": "\u05e7\u05d5\u05d3 PIN", "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP", "name": "\u05e9\u05dd", "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" diff --git a/homeassistant/components/rachio/translations/he.json b/homeassistant/components/rachio/translations/he.json new file mode 100644 index 00000000000..5247b912cc4 --- /dev/null +++ b/homeassistant/components/rachio/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/he.json b/homeassistant/components/rainmachine/translations/he.json index 3007c0e968c..fc24d9bfa43 100644 --- a/homeassistant/components/rainmachine/translations/he.json +++ b/homeassistant/components/rainmachine/translations/he.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{ip}", "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "ip_address": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/recollect_waste/translations/he.json b/homeassistant/components/recollect_waste/translations/he.json new file mode 100644 index 00000000000..cdb921611c4 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/he.json b/homeassistant/components/remote/translations/he.json index 816e0fd96bf..4b6283c1811 100644 --- a/homeassistant/components/remote/translations/he.json +++ b/homeassistant/components/remote/translations/he.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05de\u05e6\u05d1 {entity_name}", + "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + }, + "trigger_type": { + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 0f3837f3a59..a806afb6dbf 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -50,6 +50,7 @@ "automatic_add": "Automatisches Hinzuf\u00fcgen aktivieren", "debug": "Debugging aktivieren", "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen", + "event_code": "Ereigniscode zum Hinzuf\u00fcgen eingeben", "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen" }, "title": "Rfxtrx Optionen" @@ -65,7 +66,8 @@ "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll", "signal_repetitions": "Anzahl der Signalwiederholungen", "venetian_blind_mode": "Jalousie-Modus" - } + }, + "title": "Ger\u00e4teoptionen konfigurieren" } } } diff --git a/homeassistant/components/rfxtrx/translations/he.json b/homeassistant/components/rfxtrx/translations/he.json new file mode 100644 index 00000000000..645424ac9af --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "setup_network": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, + "setup_serial": { + "data": { + "device": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df" + }, + "title": "\u05d4\u05ea\u05e7\u05df" + }, + "setup_serial_manual_path": { + "data": { + "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + }, + "title": "\u05e0\u05ea\u05d9\u05d1" + } + } + }, + "options": { + "error": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index 938c471e992..4d2ae4710e7 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -5,9 +5,13 @@ "cannot_connect": "Impossibile connettersi" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "one": "Pi\u00f9", + "other": "Altri" }, "step": { + "one": "Pi\u00f9", + "other": "Altri", "setup_network": { "data": { "host": "Host", @@ -35,6 +39,7 @@ } } }, + "one": "Pi\u00f9", "options": { "error": { "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", @@ -70,5 +75,6 @@ "title": "Configurare le opzioni del dispositivo" } } - } + }, + "other": "Altri" } \ No newline at end of file diff --git a/homeassistant/components/ring/translations/he.json b/homeassistant/components/ring/translations/he.json index ac90b3264ea..e428d0009ae 100644 --- a/homeassistant/components/ring/translations/he.json +++ b/homeassistant/components/ring/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 8e50b61f16f..424a93f3eb7 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -33,8 +33,10 @@ "init": { "data": { "code_arm_required": "PIN-Code zum Entsperren vorgeben", - "code_disarm_required": "PIN-Code zum Entsperren vorgeben" - } + "code_disarm_required": "PIN-Code zum Entsperren vorgeben", + "scan_interval": "Wie oft Risco abgefragt werden soll (in Sekunden)" + }, + "title": "Optionen konfigurieren" }, "risco_to_ha": { "data": { diff --git a/homeassistant/components/risco/translations/he.json b/homeassistant/components/risco/translations/he.json new file mode 100644 index 00000000000..ed04412d69b --- /dev/null +++ b/homeassistant/components/risco/translations/he.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "armed_custom_bypass": "\u05d3\u05e8\u05d5\u05da \u05de\u05e2\u05e7\u05e3 \u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea", + "armed_home": "\u05d4\u05d1\u05d9\u05ea \u05d3\u05e8\u05d5\u05da", + "armed_night": "\u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4" + } + }, + "risco_to_ha": { + "data": { + "A": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d0'", + "B": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d1'", + "C": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d2'" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/de.json b/homeassistant/components/rituals_perfume_genie/translations/de.json index 67b8ed59e0b..72f18702457 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/de.json +++ b/homeassistant/components/rituals_perfume_genie/translations/de.json @@ -13,7 +13,8 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "title": "Verbinden Sie sich mit Ihrem Rituals-Konto" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/he.json b/homeassistant/components/rituals_perfume_genie/translations/he.json new file mode 100644 index 00000000000..ecb8a74bc6f --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json new file mode 100644 index 00000000000..887a102c99a --- /dev/null +++ b/homeassistant/components/roku/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 5d833960240..8c2d9d4e84a 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -11,10 +11,18 @@ "flow_title": "{name}", "step": { "discovery_confirm": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + }, "description": "Vuoi configurare {name}?", "title": "Roku" }, "ssdp_confirm": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + }, "description": "Vuoi impostare {name}?", "title": "Roku" }, diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index b66a6681be7..3067c968ff3 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -34,6 +34,7 @@ "blid": "BLID", "host": "Host" }, + "description": "Es wurde kein Roomba oder Braava in Ihrem Netzwerk entdeckt. Die BLID ist der Teil des Ger\u00e4te-Hostnamens nach `iRobot-` oder `Roomba-`. Bitte folgen Sie den Schritten, die in der Dokumentation unter: {auth_help_url}", "title": "Manuell mit dem Ger\u00e4t verbinden" }, "user": { diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json index 3007c0e968c..e2d0c48b0b9 100644 --- a/homeassistant/components/roomba/translations/he.json +++ b/homeassistant/components/roomba/translations/he.json @@ -1,10 +1,40 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name} ({host})", "step": { - "user": { + "init": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, + "link": { + "title": "\u05d0\u05d7\u05d6\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4" + }, + "link_manual": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: {auth_help_url}", + "title": "\u05d4\u05d6\u05df \u05e1\u05d9\u05e1\u05de\u05d4" + }, + "manual": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05db\u05e8\u05d2\u05e2 \u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4-BLID \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d5\u05d0 \u05ea\u05d4\u05dc\u05d9\u05da \u05d9\u05d3\u05e0\u05d9. \u05e0\u05d0 \u05d1\u05e6\u05e2 \u05d0\u05ea \u05d4\u05e9\u05dc\u05d1\u05d9\u05dd \u05d4\u05de\u05ea\u05d5\u05d0\u05e8\u05d9\u05dd \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" } } } diff --git a/homeassistant/components/roon/translations/he.json b/homeassistant/components/roon/translations/he.json new file mode 100644 index 00000000000..6df3cf0a9f9 --- /dev/null +++ b/homeassistant/components/roon/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "link": { + "description": "\u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e9\u05e8 \u05dc-Home Assistant \u05d1-Roon. \u05dc\u05d0\u05d7\u05e8 \u05e9\u05ea\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd Roon Core, \u05e4\u05ea\u05d7 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d5\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea HomeAssistant \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d4\u05e8\u05d7\u05d1\u05d5\u05ea." + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d2\u05dc\u05d5\u05ea \u05d0\u05ea \u05e9\u05e8\u05ea Roon, \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d4-IP." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/he.json b/homeassistant/components/ruckus_unleashed/translations/he.json index 6ef580c7d8d..479d2f2f5e8 100644 --- a/homeassistant/components/ruckus_unleashed/translations/he.json +++ b/homeassistant/components/ruckus_unleashed/translations/he.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index 3ba569c87db..aa811944700 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -3,22 +3,31 @@ "abort": { "already_configured": "Dieser Samsung TV ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", + "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", "cannot_connect": "Verbindung fehlgeschlagen", - "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." + "id_missing": "Dieses Samsung-Ger\u00e4t hat keine Seriennummer.", + "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt.", + "reauth_successful": "Erneute Authentifizierung war erfolgreich", + "unknown": "Unerwarteter Fehler" }, - "flow_title": "Samsung TV: {model}", + "error": { + "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", + "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Akzeptieren Sie nach dem Absenden die Meldung auf {device}, das eine Autorisierung innerhalb von 30 Sekunden anfordert." + }, "user": { "data": { "host": "Host", "name": "Name" }, - "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt." + "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Authentifizierung fragt." } } } diff --git a/homeassistant/components/samsungtv/translations/he.json b/homeassistant/components/samsungtv/translations/he.json new file mode 100644 index 00000000000..77a196b4ad9 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "auth_missing": "Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05d6\u05d5 \u05e9\u05dc Samsung. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05d7\u05d9\u05e6\u05d5\u05e0\u05d9\u05d9\u05dd \u05e9\u05dc \u05d4\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d0\u05e9\u05e8 \u05d0\u05ea Home Assistant.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "auth_missing": "Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05d6\u05d5 \u05e9\u05dc Samsung. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05d7\u05d9\u05e6\u05d5\u05e0\u05d9\u05d9\u05dd \u05e9\u05dc \u05d4\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d0\u05e9\u05e8 \u05d0\u05ea Home Assistant." + }, + "flow_title": "{device}", + "step": { + "confirm": { + "title": "\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05e9\u05dc \u05e1\u05de\u05e1\u05d5\u05e0\u05d2" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/he.json b/homeassistant/components/screenlogic/translations/he.json new file mode 100644 index 00000000000..8e592e373e6 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.he.json b/homeassistant/components/season/translations/sensor.he.json new file mode 100644 index 00000000000..a908ece48de --- /dev/null +++ b/homeassistant/components/season/translations/sensor.he.json @@ -0,0 +1,16 @@ +{ + "state": { + "season__season": { + "autumn": "\u05e1\u05ea\u05d9\u05d5", + "spring": "\u05d0\u05d1\u05d9\u05d1", + "summer": "\u05e7\u05d9\u05e5", + "winter": "\u05d7\u05d5\u05e8\u05e3" + }, + "season__season__": { + "autumn": "\u05e1\u05ea\u05d9\u05d5", + "spring": "\u05d0\u05d1\u05d9\u05d1", + "summer": "\u05e7\u05d9\u05e5", + "winter": "\u05d7\u05d5\u05e8\u05e3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json index 3007c0e968c..ecb8a74bc6f 100644 --- a/homeassistant/components/sense/translations/he.json +++ b/homeassistant/components/sense/translations/he.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index 8fbcfc1eaa2..62aca6a65e9 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -16,5 +16,21 @@ "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Optionaler Name der Umgebung.", + "event_custom_components": "Senden von Ereignissen aus benutzerdefinierten Komponenten", + "event_handled": "Behandelte Ereignisse senden", + "event_third_party_packages": "Senden von Ereignissen aus Drittanbieterpaketen", + "logging_event_level": "Der Log-Level, f\u00fcr den Sentry ein Ereignis registrieren wird", + "logging_level": "Der Log-Level, f\u00fcr den Sentry Protokolle als Breadcrums aufzeichnen wird", + "tracing": "Aktivieren des Leistungs-Tracings", + "tracing_sample_rate": "Tracing Abtastrate; zwischen 0,0 und 1,0 (1,0 = 100 %)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/he.json b/homeassistant/components/sentry/translations/he.json new file mode 100644 index 00000000000..3383895686e --- /dev/null +++ b/homeassistant/components/sentry/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/he.json b/homeassistant/components/sharkiq/translations/he.json new file mode 100644 index 00000000000..624c820b177 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 7e7cdb89f66..70053a86144 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -12,7 +12,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "M\u00f6chten Sie das {Modell} bei {Host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." + "description": "M\u00f6chten Sie das {model} bei {host} einrichten?\n\nBatteriebetriebene Ger\u00e4te, die passwortgesch\u00fctzt sind, m\u00fcssen aufgeweckt werden, bevor Sie mit dem Einrichten fortfahren.\nBatteriebetriebene Ger\u00e4te, die nicht passwortgesch\u00fctzt sind, werden hinzugef\u00fcgt, wenn das Ger\u00e4t aufwacht. Sie k\u00f6nnen das Ger\u00e4t nun manuell \u00fcber eine Taste am Ger\u00e4t aufwecken oder auf das n\u00e4chste Datenupdate des Ger\u00e4ts warten." }, "credentials": { "data": { diff --git a/homeassistant/components/shelly/translations/he.json b/homeassistant/components/shelly/translations/he.json new file mode 100644 index 00000000000..46ab05b4045 --- /dev/null +++ b/homeassistant/components/shelly/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} \u05d1-{host}? \n\n\u05d9\u05e9 \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05de\u05d5\u05d2\u05e0\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05e4\u05e0\u05d9 \u05e9\u05de\u05de\u05e9\u05d9\u05db\u05d9\u05dd \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4.\n\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05e9\u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05d2\u05e0\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d4 \u05d9\u05ea\u05d5\u05d5\u05e1\u05e4\u05d5 \u05db\u05d0\u05e9\u05e8 \u05d4\u05d4\u05ea\u05e7\u05df \u05d9\u05ea\u05e2\u05d5\u05e8\u05e8, \u05db\u05e2\u05ea \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05d7\u05db\u05d5\u05ea \u05dc\u05e2\u05d3\u05db\u05d5\u05df \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d4\u05d1\u05d0 \u05de\u05d4\u05d4\u05ea\u05e7\u05df." + }, + "credentials": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/he.json b/homeassistant/components/shopping_list/translations/he.json new file mode 100644 index 00000000000..6c51baa2a56 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05e7\u05e0\u05d9\u05d5\u05ea?", + "title": "\u05e8\u05e9\u05d9\u05de\u05ea \u05e7\u05e0\u05d9\u05d5\u05ea" + } + } + }, + "title": "\u05e8\u05e9\u05d9\u05de\u05ea \u05e7\u05e0\u05d9\u05d5\u05ea" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json new file mode 100644 index 00000000000..6da5a2c4750 --- /dev/null +++ b/homeassistant/components/sia/translations/de.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwenden Sie nur 0-9 und A-F.", + "invalid_account_length": "Das Konto hat nicht die richtige L\u00e4nge. Es muss zwischen 3 und 16 Zeichen lang sein.", + "invalid_key_format": "Der Schl\u00fcssel ist kein Hex-Wert, bitte verwenden Sie nur 0-9 und A-F.", + "invalid_key_length": "Der Schl\u00fcssel hat nicht die richtige L\u00e4nge. Er muss 16, 24 oder 32 Hex-Zeichen lang sein.", + "invalid_ping": "Das Ping-Intervall muss zwischen 1 und 1440 Minuten liegen.", + "invalid_zones": "Es muss mindestens eine Zone vorhanden sein.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "additional_account": { + "data": { + "account": "Konto-ID", + "additional_account": "Zus\u00e4tzliche Konten", + "encryption_key": "Verschl\u00fcsselungscode", + "ping_interval": "Ping-Intervall (min)", + "zones": "Anzahl an Zonen f\u00fcr das Konto" + }, + "title": "Dem aktuellen Port ein weiteres Konto hinzuf\u00fcgen." + }, + "user": { + "data": { + "account": "Konto-ID", + "additional_account": "Zus\u00e4tzliche Konten", + "encryption_key": "Verschl\u00fcsselungscode", + "ping_interval": "Ping-Intervall (min)", + "port": "Port", + "protocol": "Protokoll", + "zones": "Anzahl an Zonen f\u00fcr das Konto" + }, + "title": "Erstellen einer Verbindung f\u00fcr SIA-basierte Alarmsysteme." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorieren der Zeitstempelpr\u00fcfung der SIA-Ereignisse", + "zones": "Anzahl an Zonen f\u00fcr das Konto" + }, + "description": "Stellen Sie die Optionen f\u00fcr das Konto {account} ein:", + "title": "Optionen f\u00fcr das SIA-Setup." + } + } + }, + "title": "SIA Alarmsysteme" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/he.json b/homeassistant/components/sia/translations/he.json new file mode 100644 index 00000000000..65f845f99c1 --- /dev/null +++ b/homeassistant/components/sia/translations/he.json @@ -0,0 +1,38 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "additional_account": { + "data": { + "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df", + "additional_account": "\u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd", + "encryption_key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e6\u05e4\u05e0\u05d4", + "ping_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e4\u05d9\u05e0\u05d2 (\u05de\u05d9\u05e0\u05d9\u05de\u05d5\u05dd)", + "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df" + } + }, + "user": { + "data": { + "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df", + "additional_account": "\u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd", + "encryption_key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e6\u05e4\u05e0\u05d4", + "ping_interval": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e4\u05d9\u05e0\u05d2 (\u05de\u05d9\u05e0\u05d9\u05de\u05d5\u05dd)", + "port": "\u05e4\u05ea\u05d7\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df" + } + } + } + }, + "options": { + "step": { + "options": { + "data": { + "zones": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05dc\u05d7\u05e9\u05d1\u05d5\u05df" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index 3007c0e968c..f17c3879f5a 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -1,9 +1,19 @@ { "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { - "user": { + "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da." + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" } } } diff --git a/homeassistant/components/sma/translations/he.json b/homeassistant/components/sma/translations/he.json new file mode 100644 index 00000000000..ba081d16366 --- /dev/null +++ b/homeassistant/components/sma/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/he.json b/homeassistant/components/smappee/translations/he.json new file mode 100644 index 00000000000..08973c08fa6 --- /dev/null +++ b/homeassistant/components/smappee/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" + }, + "flow_title": "{name}", + "step": { + "environment": { + "data": { + "environment": "\u05e1\u05d1\u05d9\u05d1\u05d4" + } + }, + "local": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d0\u05ea \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05e6\u05d9\u05d4 \u05d4\u05de\u05e7\u05d5\u05de\u05d9\u05ea \u05e9\u05dc Smappee" + }, + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/he.json b/homeassistant/components/smart_meter_texas/translations/he.json new file mode 100644 index 00000000000..c479d8488f2 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/he.json b/homeassistant/components/smarthab/translations/he.json new file mode 100644 index 00000000000..2aa9a81dcd6 --- /dev/null +++ b/homeassistant/components/smarthab/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index 5cb63393be8..56fd6e0b4fb 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -8,6 +8,16 @@ "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05e7\u05e6\u05d4 \u05e9\u05d4\u05d5\u05d2\u05d3\u05e8\u05d4 \u05d1- `base_url`. \u05e2\u05d9\u05d9\u05df \u05d1\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05e8\u05db\u05d9\u05d1." }, "step": { + "pat": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + } + }, + "select_location": { + "data": { + "location_id": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + }, "user": { "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 " diff --git a/homeassistant/components/smarttub/translations/he.json b/homeassistant/components/smarttub/translations/he.json new file mode 100644 index 00000000000..01ef4d534f6 --- /dev/null +++ b/homeassistant/components/smarttub/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc SmartTub \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/he.json b/homeassistant/components/smhi/translations/he.json index 4c49313d977..99eeb837dc3 100644 --- a/homeassistant/components/smhi/translations/he.json +++ b/homeassistant/components/smhi/translations/he.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/sms/translations/he.json b/homeassistant/components/sms/translations/he.json new file mode 100644 index 00000000000..7d80432febf --- /dev/null +++ b/homeassistant/components/sms/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/he.json b/homeassistant/components/solaredge/translations/he.json new file mode 100644 index 00000000000..3780025080f --- /dev/null +++ b/homeassistant/components/solaredge/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/he.json b/homeassistant/components/solarlog/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/solarlog/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/he.json b/homeassistant/components/soma/translations/he.json new file mode 100644 index 00000000000..b2a3b1b660e --- /dev/null +++ b/homeassistant/components/soma/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/he.json b/homeassistant/components/somfy/translations/he.json new file mode 100644 index 00000000000..eef0bf816f4 --- /dev/null +++ b/homeassistant/components/somfy/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index 4382ccd4a0c..d66be000314 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -15,7 +15,8 @@ "host": "Host", "port": "Port", "system_id": "System-ID" - } + }, + "description": "Die System-ID kann in der MyLink-App unter Integration durch Auswahl eines beliebigen Nicht-Cloud-Dienstes abgerufen werden." } } }, @@ -25,16 +26,24 @@ }, "step": { "entity_config": { + "data": { + "reverse": "Jalousie ist invertiert" + }, "description": "Optionen f\u00fcr `{entity_id}` konfigurieren", "title": "Entit\u00e4t konfigurieren" }, "init": { "data": { - "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t." + "default_reverse": "Standardinvertierungsstatus f\u00fcr nicht konfigurierte Abdeckungen", + "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t.", + "target_id": "Konfigurieren der Optionen f\u00fcr eine Jalousie." }, "title": "MyLink-Optionen konfigurieren" }, "target_config": { + "data": { + "reverse": "Jalousie ist invertiert" + }, "description": "Konfiguriere die Optionen f\u00fcr `{target_name}`", "title": "MyLink-Cover konfigurieren" } diff --git a/homeassistant/components/somfy_mylink/translations/he.json b/homeassistant/components/somfy_mylink/translations/he.json index 9af5985ac45..f47bfa240ba 100644 --- a/homeassistant/components/somfy_mylink/translations/he.json +++ b/homeassistant/components/somfy_mylink/translations/he.json @@ -1,5 +1,25 @@ { + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "system_id": "\u05de\u05d6\u05d4\u05d4 \u05de\u05e2\u05e8\u05db\u05ea" + } + } + } + }, "options": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "init": { "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc MyLink" diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json new file mode 100644 index 00000000000..41569a46644 --- /dev/null +++ b/homeassistant/components/sonarr/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json index b18a6bbcd3a..329263d1bc3 100644 --- a/homeassistant/components/songpal/translations/he.json +++ b/homeassistant/components/songpal/translations/he.json @@ -5,6 +5,12 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name} ({host})", + "step": { + "init": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/spider/translations/he.json b/homeassistant/components/spider/translations/he.json new file mode 100644 index 00000000000..977aa3a9d5e --- /dev/null +++ b/homeassistant/components/spider/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 1ed426282fe..f92db780e82 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -14,7 +14,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "description": "Die Spotify-Integration muss sich bei Spotify f\u00fcr das Konto neu authentifizieren: {Konto}", + "description": "Die Spotify-Integration muss sich bei Spotify f\u00fcr das Konto neu authentifizieren: {account}", "title": "Integration erneut authentifizieren" } } diff --git a/homeassistant/components/spotify/translations/he.json b/homeassistant/components/spotify/translations/he.json new file mode 100644 index 00000000000..a2605ed88f6 --- /dev/null +++ b/homeassistant/components/spotify/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index c64e1ae3a1c..295aeddfa86 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -10,7 +10,7 @@ "no_server_found": "Konnte den Server nicht automatisch entdecken.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Logitech Squeezebox", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/he.json b/homeassistant/components/squeezebox/translations/he.json new file mode 100644 index 00000000000..0a0ab6a9b5b --- /dev/null +++ b/homeassistant/components/squeezebox/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{host}", + "step": { + "edit": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/he.json b/homeassistant/components/srp_energy/translations/he.json new file mode 100644 index 00000000000..b38e5b12c8b --- /dev/null +++ b/homeassistant/components/srp_energy/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/he.json b/homeassistant/components/starline/translations/he.json index 53542798232..06aacb32caa 100644 --- a/homeassistant/components/starline/translations/he.json +++ b/homeassistant/components/starline/translations/he.json @@ -1,11 +1,18 @@ { "config": { + "error": { + "error_auth_user": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd" + }, "step": { + "auth_captcha": { + "description": "{captcha_img}" + }, "auth_user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "description": "\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05e1\u05d9\u05e1\u05de\u05d4 \u05dc\u05d7\u05e9\u05d1\u05d5\u05df StarLine" } } } diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index dd2fb797dbc..ac953654565 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -15,7 +15,8 @@ "data": { "pin": "PIN" }, - "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben" + "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben", + "title": "Subaru Starlink Konfiguration" }, "user": { "data": { diff --git a/homeassistant/components/subaru/translations/he.json b/homeassistant/components/subaru/translations/he.json new file mode 100644 index 00000000000..857993bee9d --- /dev/null +++ b/homeassistant/components/subaru/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/he.json b/homeassistant/components/syncthing/translations/he.json new file mode 100644 index 00000000000..7e87dacd7e5 --- /dev/null +++ b/homeassistant/components/syncthing/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + }, + "title": "\u05e1\u05d9\u05e0\u05db\u05e8\u05d5\u05df" +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/he.json b/homeassistant/components/syncthru/translations/he.json new file mode 100644 index 00000000000..3e7be115406 --- /dev/null +++ b/homeassistant/components/syncthru/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "\u05e9\u05dd", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e9\u05e7 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8" + } + }, + "user": { + "data": { + "name": "\u05e9\u05dd", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05de\u05e9\u05e7 \u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json index 8135fba13e0..a671684a770 100644 --- a/homeassistant/components/synology_dsm/translations/he.json +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -1,16 +1,34 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "missing_data": "\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d7\u05e1\u05e8\u05d9\u05dd: \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8 \u05d0\u05d5 \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05e8\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} ({host})", "step": { "link": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" }, "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } } diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json index 9f03951c429..dc85589dd32 100644 --- a/homeassistant/components/system_bridge/translations/de.json +++ b/homeassistant/components/system_bridge/translations/de.json @@ -16,7 +16,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Bitte gib den API-Schl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {Name} festgelegt hast." + "description": "Bitte gib den API-Schl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {name} festgelegt hast." }, "user": { "data": { @@ -27,5 +27,6 @@ "description": "Bitte gib Verbindungsdaten ein." } } - } + }, + "title": "System-Br\u00fccke" } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/he.json b/homeassistant/components/system_bridge/translations/he.json new file mode 100644 index 00000000000..5eb235ffb8f --- /dev/null +++ b/homeassistant/components/system_bridge/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05de\u05e4\u05ea\u05d7 API \u05e9\u05d4\u05d2\u05d3\u05e8\u05ea \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05e2\u05d1\u05d5\u05e8 {name} ." + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, + "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc\u05da." + } + } + }, + "title": "\u05d2\u05e9\u05e8 \u05d4\u05de\u05e2\u05e8\u05db\u05ea" +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/he.json b/homeassistant/components/tado/translations/he.json index ac90b3264ea..a4d57c233d4 100644 --- a/homeassistant/components/tado/translations/he.json +++ b/homeassistant/components/tado/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tag/translations/he.json b/homeassistant/components/tag/translations/he.json new file mode 100644 index 00000000000..209a1f54193 --- /dev/null +++ b/homeassistant/components/tag/translations/he.json @@ -0,0 +1,3 @@ +{ + "title": "\u05ea\u05d2" +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json index 86c3383f912..7e654d982c5 100644 --- a/homeassistant/components/tasmota/translations/de.json +++ b/homeassistant/components/tasmota/translations/de.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "invalid_discovery_topic": "Ung\u00fcltiges Discovery-Topic-Pr\u00e4fix." + }, "step": { "config": { "data": { diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json new file mode 100644 index 00000000000..16e1fafbdc8 --- /dev/null +++ b/homeassistant/components/tasmota/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "config": { + "title": "Tasmota" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/he.json b/homeassistant/components/tellduslive/translations/he.json new file mode 100644 index 00000000000..db5a0aad8d9 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05e8\u05d9\u05e7", + "title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05e7\u05e6\u05d4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json index ac90b3264ea..1d26aa8eaf0 100644 --- a/homeassistant/components/tesla/translations/he.json +++ b/homeassistant/components/tesla/translations/he.json @@ -4,7 +4,7 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "username": "\u05d3\u05d5\u05d0\"\u05dc" } } } diff --git a/homeassistant/components/tibber/translations/he.json b/homeassistant/components/tibber/translations/he.json index a2804e54143..780c7990217 100644 --- a/homeassistant/components/tibber/translations/he.json +++ b/homeassistant/components/tibber/translations/he.json @@ -4,6 +4,8 @@ "already_configured": "\u05d7\u05e9\u05d1\u05d5\u05df Tibber \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05beTibber" }, "step": { diff --git a/homeassistant/components/tile/translations/he.json b/homeassistant/components/tile/translations/he.json new file mode 100644 index 00000000000..adb5e510107 --- /dev/null +++ b/homeassistant/components/tile/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index daeead855c3..c76bab5ef91 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -15,6 +15,9 @@ }, "description": "W\u00e4hlen Sie die Vereinbarungsadresse aus, die du hinzuf\u00fcgen m\u00f6chtest.", "title": "W\u00e4hle deine Vereinbarung" + }, + "pick_implementation": { + "title": "W\u00e4hlen Sie Ihren Mandanten f\u00fcr die Authentifizierung aus" } } } diff --git a/homeassistant/components/toon/translations/he.json b/homeassistant/components/toon/translations/he.json new file mode 100644 index 00000000000..967c4a9d7ce --- /dev/null +++ b/homeassistant/components/toon/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 2f484f2a8ed..b6543752043 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -11,9 +11,10 @@ "step": { "locations": { "data": { - "location": "Standort" + "location": "Standort", + "usercode": "Benutzercode" }, - "description": "Geben Sie den Benutzercode f\u00fcr diesen Benutzer an dieser Stelle ein", + "description": "Geben Sie den Benutzercode f\u00fcr den Benutzer {location_id} an dieser Stelle ein", "title": "Standort-Benutzercodes" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json index ed07845a182..3c1c1fcbfda 100644 --- a/homeassistant/components/totalconnect/translations/he.json +++ b/homeassistant/components/totalconnect/translations/he.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "step": { "locations": { "data": { - "location": "\u05de\u05d9\u05e7\u05d5\u05dd" + "location": "\u05de\u05d9\u05e7\u05d5\u05dd", + "usercode": "\u05e7\u05d5\u05d3 \u05de\u05e9\u05ea\u05de\u05e9" } }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/tradfri/translations/he.json b/homeassistant/components/tradfri/translations/he.json index f1731579816..86006674421 100644 --- a/homeassistant/components/tradfri/translations/he.json +++ b/homeassistant/components/tradfri/translations/he.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { - "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_key": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc \u05e2\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05e1\u05d5\u05e4\u05e7. \u05d0\u05dd \u05d6\u05d4 \u05e7\u05d5\u05e8\u05d4 \u05e9\u05d5\u05d1, \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05de\u05d2\u05e9\u05e8.", "timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05e7\u05d5\u05d3" }, diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index 6f4191da70d..c4578e1c322 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 8aa7c08f352..289d2661485 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -40,12 +40,14 @@ "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin", "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", + "set_temp_divided": "Geteilten Temperaturwert f\u00fcr Solltemperaturbefehl verwenden", "support_color": "Farbunterst\u00fctzung erzwingen", "temp_divider": "Teiler f\u00fcr Temperaturwerte (0 = Standard verwenden)", + "temp_step_override": "Zieltemperaturschritt", "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur", "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit" }, - "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{Ger\u00e4tename}` konfigurieren", + "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{device_name}` vom Typ: {device_type}konfigurieren", "title": "Tuya-Ger\u00e4t konfigurieren" }, "init": { diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 98dafb882c7..29ea89d824d 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, "step": { "user": { "data": { @@ -10,5 +13,10 @@ } } } + }, + "options": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/he.json b/homeassistant/components/twentemilieu/translations/he.json new file mode 100644 index 00000000000..6faac53655f --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/he.json b/homeassistant/components/twilio/translations/he.json new file mode 100644 index 00000000000..b109b127dec --- /dev/null +++ b/homeassistant/components/twilio/translations/he.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/he.json b/homeassistant/components/twinkly/translations/he.json new file mode 100644 index 00000000000..db9e846ce56 --- /dev/null +++ b/homeassistant/components/twinkly/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "device_exists": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7 (\u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP) \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05e0\u05e6\u05e0\u05e5 \u05e9\u05dc\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 4c34101a7ce..39ac9e5c946 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -64,7 +64,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients" + "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients", + "allow_uptime_sensors": "Uptime-Sensoren f\u00fcr Netzwerk-Clients" }, "description": "Konfigurieren Sie Statistiksensoren", "title": "UniFi-Optionen 3/3" diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json index 3007c0e968c..83c34cb9c77 100644 --- a/homeassistant/components/unifi/translations/he.json +++ b/homeassistant/components/unifi/translations/he.json @@ -1,9 +1,48 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "faulty_credentials": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "service_unavailable": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{site} ({host})", "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea" + } + }, + "device_tracker": { + "data": { + "track_clients": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea", + "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)" + } + }, + "simple_options": { + "data": { + "block_client": "\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05de\u05d1\u05d5\u05e7\u05e8\u05d9\u05dd \u05e9\u05dc \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e8\u05e9\u05ea", + "track_clients": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea", + "track_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9 \u05e8\u05e9\u05ea (\u05d4\u05ea\u05e7\u05e0\u05d9 Ubiquiti)" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u05d7\u05d9\u05d9\u05e9\u05e0\u05d9 \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e8\u05d5\u05d7\u05d1 \u05e4\u05e1 \u05dc\u05dc\u05e7\u05d5\u05d7\u05d5\u05ea \u05e8\u05e9\u05ea" } } } diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 007da9f80ed..00672b65b4e 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -48,6 +48,12 @@ "description": "Configurare il tracciamento del dispositivo", "title": "Opzioni UniFi 1/3" }, + "init": { + "data": { + "one": "Pi\u00f9", + "other": "Altri" + } + }, "simple_options": { "data": { "block_client": "Client controllati per l'accesso alla rete", diff --git a/homeassistant/components/upb/translations/he.json b/homeassistant/components/upb/translations/he.json index ece7d57a907..e89a02adfa4 100644 --- a/homeassistant/components/upb/translations/he.json +++ b/homeassistant/components/upb/translations/he.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4" }, "step": { diff --git a/homeassistant/components/upcloud/translations/he.json b/homeassistant/components/upcloud/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/upcloud/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index 8a5f4fefadf..2d2a97b0943 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Aktualisierungsintervall (Sekunden, mindestens 30)", + "unique_id": "Ger\u00e4t", "usn": "Ger\u00e4t" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsintervall (Sekunden, minimal 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index 4b922ccd2ba..706d87f0db4 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "many": "", + "one": "\u05e8\u05d9\u05e7", + "other": "\u05e8\u05d9\u05e7\u05d9\u05dd", + "two": "\u05e8\u05d9\u05e7\u05d9\u05dd" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "unique_id": "\u05d4\u05ea\u05e7\u05df", "usn": "\u05de\u05db\u05e9\u05d9\u05e8" } } diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index 67a3a385dbc..0e00d002422 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -11,6 +11,10 @@ }, "flow_title": "{name}", "step": { + "init": { + "one": "Pi\u00f9", + "other": "Altri" + }, "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" }, diff --git a/homeassistant/components/verisure/translations/he.json b/homeassistant/components/verisure/translations/he.json new file mode 100644 index 00000000000..ba0af14615f --- /dev/null +++ b/homeassistant/components/verisure/translations/he.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/he.json b/homeassistant/components/vesync/translations/he.json index 6f4191da70d..a5aa8e05f22 100644 --- a/homeassistant/components/vesync/translations/he.json +++ b/homeassistant/components/vesync/translations/he.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + }, + "title": "\u05d4\u05d6\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4" } } } diff --git a/homeassistant/components/vilfo/translations/he.json b/homeassistant/components/vilfo/translations/he.json new file mode 100644 index 00000000000..20b08543899 --- /dev/null +++ b/homeassistant/components/vilfo/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e0\u05ea\u05d1 \u05e9\u05dc Vilfo. \u05d0\u05ea\u05d4 \u05e6\u05e8\u05d9\u05da \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7/IP \u05e9\u05dc \u05e0\u05ea\u05d1 Vilfo \u05e9\u05dc\u05da \u05d5\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05ea API. \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05e2\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05e7\u05d1\u05dc \u05e4\u05e8\u05d8\u05d9\u05dd \u05d0\u05dc\u05d4, \u05d1\u05e7\u05e8 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/he.json b/homeassistant/components/vizio/translations/he.json new file mode 100644 index 00000000000..8d030e5662e --- /dev/null +++ b/homeassistant/components/vizio/translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "pair_tv": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/he.json b/homeassistant/components/volumio/translations/he.json new file mode 100644 index 00000000000..48c198c0a7f --- /dev/null +++ b/homeassistant/components/volumio/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/de.json b/homeassistant/components/wallbox/translations/de.json new file mode 100644 index 00000000000..89362597b85 --- /dev/null +++ b/homeassistant/components/wallbox/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "station": "Seriennummer", + "username": "Benutzername" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/he.json b/homeassistant/components/wallbox/translations/he.json new file mode 100644 index 00000000000..ca1c7a93c5c --- /dev/null +++ b/homeassistant/components/wallbox/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, + "title": "\u05ea\u05d9\u05d1\u05ea \u05e7\u05d9\u05e8" +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/he.json b/homeassistant/components/water_heater/translations/he.json new file mode 100644 index 00000000000..4b9d5bf4491 --- /dev/null +++ b/homeassistant/components/water_heater/translations/he.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" + } + }, + "state": { + "_": { + "off": "\u05db\u05d1\u05d5\u05d9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json index ac2a911d233..2af713eb5d1 100644 --- a/homeassistant/components/waze_travel_time/translations/de.json +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -13,8 +13,27 @@ "name": "Name", "origin": "Startort", "region": "Region" - } + }, + "description": "Geben Sie f\u00fcr Ursprung und Ziel die Adresse oder die GPS-Koordinaten des Standorts ein (GPS-Koordinaten m\u00fcssen durch ein Komma getrennt werden). Sie k\u00f6nnen auch eine Entity-ID eingeben, die diese Informationen in ihrem Zustand bereitstellt, eine Entity-ID mit den Attributen Breitengrad und L\u00e4ngengrad oder einen Zonen-Namen." } } - } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "F\u00e4hren meiden?", + "avoid_subscription_roads": "Stra\u00dfen vermeiden, die eine Vignette/ ein Abonnement ben\u00f6tigen?", + "avoid_toll_roads": "Mautstra\u00dfen meiden?", + "excl_filter": "Substring NICHT in Beschreibung der ausgew\u00e4hlten Route", + "incl_filter": "Substring in Beschreibung der ausgew\u00e4hlten Route", + "realtime": "Reisezeit in Echtzeit?", + "units": "Einheiten", + "vehicle_type": "Fahrzeugtyp" + }, + "description": "Mit den \"Substring\"-Eintr\u00e4gen k\u00f6nnen Sie die Integration zwingen, eine bestimmte Route zu verwenden oder eine bestimmte Route bei der Zeitreiseberechnung zu vermeiden." + } + } + }, + "title": "Waze Reisezeit" } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/he.json b/homeassistant/components/waze_travel_time/translations/he.json new file mode 100644 index 00000000000..a7a3d83cfa2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 59838d9ee86..f79d960c182 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "not_supported_device": "Dieses WiLight wird derzeit nicht unterst\u00fctzt", + "not_wilight_device": "Dieses Ger\u00e4t ist kein WiLight" }, "flow_title": "WiLight: {name}", "step": { diff --git a/homeassistant/components/wilight/translations/he.json b/homeassistant/components/wilight/translations/he.json new file mode 100644 index 00000000000..977167ec765 --- /dev/null +++ b/homeassistant/components/wilight/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json new file mode 100644 index 00000000000..971eda3486f --- /dev/null +++ b/homeassistant/components/withings/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{profile}", + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/he.json b/homeassistant/components/wled/translations/he.json new file mode 100644 index 00000000000..887a102c99a --- /dev/null +++ b/homeassistant/components/wled/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/he.json b/homeassistant/components/wolflink/translations/he.json new file mode 100644 index 00000000000..c479d8488f2 --- /dev/null +++ b/homeassistant/components/wolflink/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index a83fb9856ad..6b1baf8b0bf 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -11,11 +11,15 @@ "at_frostschutz": "AT Frostschutz", "aus": "Aus", "auto": "", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", "automatik_aus": "Automatik AUS", "automatik_ein": "Automatik EIN", + "bereit_keine_ladung": "Bereit, keine Ladung", "betrieb_ohne_brenner": "Betrieb ohne Brenner", "cooling": "K\u00fchlung", "deaktiviert": "Deaktiviert", + "dhw_prior": "DHWPrior", "eco": "Eco", "ein": "Ein", "estrichtrocknung": "Estrichtrocknung", @@ -25,6 +29,7 @@ "frost_warmwasser": "Warmwasser Frost", "frostschutz": "Frostschutz", "gasdruck": "Gasdruck", + "glt_betrieb": "BMS-Modus", "gradienten_uberwachung": "Gradienten\u00fcberwachung", "heizbetrieb": "Heizbetrieb", "heizgerat_mit_speicher": "Heizger\u00e4t mit Speicher", @@ -44,6 +49,7 @@ "nur_heizgerat": "Nur Heizger\u00e4t", "parallelbetrieb": "Parallelbetrieb", "partymodus": "Party-Modus", + "perm_cooling": "PermCooling", "permanent": "Permanent", "permanentbetrieb": "Permanentbetrieb", "reduzierter_betrieb": "Reduzierter Betrieb", @@ -53,9 +59,11 @@ "schornsteinfeger": "Emissionspr\u00fcfung", "smart_grid": "SmartGrid", "smart_home": "SmartHome", + "softstart": "Soft Start", "solarbetrieb": "Solarmodus", "sparbetrieb": "Sparmodus", "sparen": "Sparen", + "spreizung_hoch": "Spreizung zu hoch", "spreizung_kf": "Spreizung KF", "stabilisierung": "Stabilisierung", "standby": "Standby", diff --git a/homeassistant/components/wolflink/translations/sensor.he.json b/homeassistant/components/wolflink/translations/sensor.he.json new file mode 100644 index 00000000000..68b635ba82b --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.he.json @@ -0,0 +1,11 @@ +{ + "state": { + "wolflink__state": { + "solarbetrieb": "\u05de\u05e6\u05d1 \u05e1\u05d5\u05dc\u05d0\u05e8\u05d9", + "standby": "\u05de\u05e6\u05d1 \u05d4\u05de\u05ea\u05e0\u05d4", + "start": "\u05d4\u05ea\u05d7\u05dc", + "warmwasser": "DHW", + "zunden": "\u05d4\u05e6\u05ea\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/he.json b/homeassistant/components/xbox/translations/he.json new file mode 100644 index 00000000000..61069036ec5 --- /dev/null +++ b/homeassistant/components/xbox/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json new file mode 100644 index 00000000000..5a12ddc3b9e --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + }, + "settings": { + "description": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05de\u05e4\u05ea\u05d7 (\u05e1\u05d9\u05e1\u05de\u05d4) \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05d3\u05e8\u05db\u05d4 \u05d6\u05d5: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u05d0\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05de\u05e1\u05d5\u05e4\u05e7, \u05e8\u05e7 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d9\u05d4\u05d9\u05d5 \u05e0\u05d2\u05d9\u05e9\u05d9\u05dd" + }, + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json new file mode 100644 index 00000000000..2521b963485 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "step": { + "device": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 1a8f9a463b7..0c9e6d2a154 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "{model} {host}", "step": { "discovery_confirm": { "description": "M\u00f6chten Sie {model} ({host}) einrichten?" diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json new file mode 100644 index 00000000000..adfd4d904ce --- /dev/null +++ b/homeassistant/components/yeelight/translations/he.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{model} {host}", + "step": { + "discovery_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + }, + "description": "\u05d0\u05dd \u05ea\u05e9\u05d0\u05d9\u05e8 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05e8\u05d9\u05e7, \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d9\u05e9\u05de\u05e9 \u05dc\u05de\u05e6\u05d9\u05d0\u05ea \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u05d3\u05d2\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "nightlight_switch": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05de\u05ea\u05d2 \u05ea\u05d0\u05d5\u05e8\u05ea \u05dc\u05d9\u05dc\u05d4", + "save_on_change": "\u05e9\u05de\u05d5\u05e8 \u05e1\u05d8\u05d8\u05d5\u05e1 \u05d1\u05e9\u05d9\u05e0\u05d5\u05d9", + "transition": "\u05d6\u05de\u05df \u05de\u05e2\u05d1\u05e8 (\u05d0\u05dc\u05e4\u05d9\u05d5\u05ea \u05e9\u05e0\u05d9\u05d4)", + "use_music_mode": "\u05d4\u05e4\u05e2\u05dc\u05ea \u05de\u05e6\u05d1 \u05de\u05d5\u05e1\u05d9\u05e7\u05d4" + }, + "description": "\u05d0\u05dd \u05ea\u05e9\u05d0\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d3\u05d2\u05dd \u05e8\u05d9\u05e7, \u05d4\u05d5\u05d0 \u05d9\u05d6\u05d5\u05d4\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index bea46792003..48a84e712f2 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -41,6 +41,8 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", + "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktivieren Sie den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "title": "Globale Optionen" diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json index 2ede9ae4430..fa40de672e2 100644 --- a/homeassistant/components/zha/translations/he.json +++ b/homeassistant/components/zha/translations/he.json @@ -1,9 +1,50 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", "step": { "port_config": { "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea" + }, + "user": { + "title": "ZHA" } } + }, + "config_panel": { + "zha_options": { + "consider_unavailable_battery": "\u05e9\u05e7\u05d5\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4 \u05db\u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d0\u05d7\u05e8 (\u05e9\u05e0\u05d9\u05d5\u05ea)", + "consider_unavailable_mains": "\u05e9\u05e7\u05d5\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05e9\u05ea \u05d7\u05e9\u05de\u05dc \u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d0\u05d7\u05e8 (\u05e9\u05e0\u05d9\u05d5\u05ea)", + "enable_identify_on_join": "\u05d0\u05e4\u05e9\u05e8 \u05d0\u05e4\u05e7\u05d8 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05db\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05de\u05e6\u05d8\u05e8\u05e4\u05d9\u05dd \u05dc\u05e8\u05e9\u05ea", + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05db\u05dc\u05dc\u05d9\u05d5\u05ea" + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u05e9\u05e0\u05d9 \u05d4\u05db\u05e4\u05ea\u05d5\u05e8\u05d9\u05dd", + "button_1": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d0\u05e9\u05d5\u05df", + "button_2": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05e0\u05d9", + "button_3": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05dc\u05d9\u05e9\u05d9", + "button_4": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d1\u05d9\u05e2\u05d9", + "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9", + "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9", + "close": "\u05e1\u05d2\u05d5\u05e8", + "dim_down": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05d8\u05d4", + "dim_up": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05e2\u05dc\u05d4", + "left": "\u05e9\u05de\u05d0\u05dc", + "open": "\u05e4\u05ea\u05d5\u05d7", + "right": "\u05d9\u05de\u05d9\u05df", + "turn_off": "\u05db\u05d1\u05d4", + "turn_on": "\u05d4\u05e4\u05e2\u05dc" + }, + "trigger_type": { + "device_dropped": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05d5\u05e9\u05de\u05d8", + "device_offline": "\u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05de\u05e7\u05d5\u05d5\u05df" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 247f6b4027e..4cdcdd654cc 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -41,6 +41,8 @@ "title": "Opzioni del pannello di controllo degli allarmi" }, "zha_options": { + "consider_unavailable_battery": "Considera i dispositivi alimentati a batteria non disponibili dopo (secondi)", + "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", "title": "Opzioni globali" @@ -82,7 +84,7 @@ "device_offline": "Dispositivo offline", "device_rotated": "Dispositivo ruotato \" {subtype} \"", "device_shaken": "Dispositivo in vibrazione", - "device_slid": "Dispositivo scivolato \"{sottotipo}\"", + "device_slid": "Dispositivo scivolato \"{subtype}\"", "device_tilted": "Dispositivo inclinato", "remote_button_alt_double_press": "Pulsante \"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", "remote_button_alt_long_press": "Pulsante \"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", diff --git a/homeassistant/components/zodiac/translations/sensor.he.json b/homeassistant/components/zodiac/translations/sensor.he.json new file mode 100644 index 00000000000..8ec13a3fcf0 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.he.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u05d3\u05dc\u05d9", + "aries": "\u05d8\u05dc\u05d4", + "cancer": "\u05e1\u05e8\u05d8\u05df", + "capricorn": "\u05d2\u05d3\u05d9", + "gemini": "\u05ea\u05d0\u05d5\u05de\u05d9\u05dd", + "leo": "\u05d0\u05e8\u05d9\u05d4", + "libra": "\u05de\u05d0\u05d6\u05e0\u05d9\u05d9\u05dd", + "pisces": "\u05d3\u05d2\u05d9\u05dd", + "sagittarius": "\u05e7\u05e9\u05ea", + "scorpio": "\u05e2\u05e7\u05e8\u05d1", + "taurus": "\u05e9\u05d5\u05e8", + "virgo": "\u05d1\u05ea\u05d5\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/he.json b/homeassistant/components/zone/translations/he.json index b6a2a30b625..99bbed91c0f 100644 --- a/homeassistant/components/zone/translations/he.json +++ b/homeassistant/components/zone/translations/he.json @@ -6,7 +6,7 @@ "step": { "init": { "data": { - "icon": "\u05e1\u05de\u05dc", + "icon": "\u05e1\u05de\u05dc\u05d9\u05dc", "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd", diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json index 5fa5d0a5234..af053e59ec3 100644 --- a/homeassistant/components/zoneminder/translations/de.json +++ b/homeassistant/components/zoneminder/translations/de.json @@ -1,22 +1,33 @@ { "config": { "abort": { + "auth_fail": "Benutzername oder Passwort sind falsch.", "cannot_connect": "Verbindung fehlgeschlagen", + "connection_error": "Es konnte keine Verbindung zu einem ZoneMinder-Server hergestellt werden.", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "create_entry": { + "default": "ZoneMinder-Server hinzugef\u00fcgt." + }, "error": { + "auth_fail": "Benutzername oder Passwort sind falsch.", "cannot_connect": "Verbindung fehlgeschlagen", + "connection_error": "Es konnte keine Verbindung zu einem ZoneMinder-Server hergestellt werden.", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "Host und Port (z. B. 10.10.0.4:8010)", "password": "Passwort", + "path": "ZM-Pfad", + "path_zms": "ZMS-Pfad", "ssl": "Nutzt ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" - } + }, + "title": "ZoneMinder Server hinzuf\u00fcgen." } } } diff --git a/homeassistant/components/zoneminder/translations/he.json b/homeassistant/components/zoneminder/translations/he.json new file mode 100644 index 00000000000..208792441f7 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "auth_fail": "\u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "error": { + "auth_fail": "\u05e9\u05dd \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05d0\u05d5 \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d9\u05dd.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7 \u05d5\u05d9\u05e6\u05d9\u05d0\u05d4 (\u05dc\u05d3\u05d5\u05d2' 10.10.0.4:8010)", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json index 4ed45b0711f..585b696b496 100644 --- a/homeassistant/components/zwave/translations/he.json +++ b/homeassistant/components/zwave/translations/he.json @@ -1,4 +1,17 @@ { + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + } + } + }, "state": { "_": { "dead": "\u05de\u05ea", diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json new file mode 100644 index 00000000000..4dbfc33457f --- /dev/null +++ b/homeassistant/components/zwave_js/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "configure_addon": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "manual": { + "data": { + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS" + }, + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS?", + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" + } + } + } +} \ No newline at end of file From 6a419483de4ff7ab2f1f99dae036518950132125 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 6 Jun 2021 00:31:11 -0600 Subject: [PATCH 196/750] Bump aiorecollect to 1.0.5 (#51538) --- homeassistant/components/recollect_waste/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index e33edcc2ab5..550612fbea2 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,7 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": ["aiorecollect==1.0.4"], + "requirements": ["aiorecollect==1.0.5"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0516c327020..0f466a7a55f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopvpc==2.1.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.4 +aiorecollect==1.0.5 # homeassistant.components.shelly aioshelly==0.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a408a9f89a9..25bda050a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiopvpc==2.1.2 aiopylgtv==0.4.0 # homeassistant.components.recollect_waste -aiorecollect==1.0.4 +aiorecollect==1.0.5 # homeassistant.components.shelly aioshelly==0.6.4 From 50001684aa0cf9bc062874b1f247e84fc8a9b1ae Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 6 Jun 2021 09:13:50 +0200 Subject: [PATCH 197/750] Add retries/retry_on_empty configuration parameters to Modbus (#51412) * Add retries/retry_on_empty configuration parameters. * Please review comment. --- homeassistant/components/modbus/__init__.py | 4 ++++ homeassistant/components/modbus/const.py | 2 ++ homeassistant/components/modbus/modbus.py | 13 ++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8d00a1ba7c5..242b5e7a0ad 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -66,6 +66,8 @@ from .const import ( CONF_MIN_TEMP, CONF_PARITY, CONF_PRECISION, + CONF_RETRIES, + CONF_RETRY_ON_EMPTY, CONF_REVERSE_ORDER, CONF_SCALE, CONF_STATE_CLOSED, @@ -292,6 +294,8 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, vol.Optional(CONF_CLOSE_COMM_ON_ERROR, default=True): cv.boolean, vol.Optional(CONF_DELAY, default=0): cv.positive_int, + vol.Optional(CONF_RETRIES, default=3): cv.positive_int, + vol.Optional(CONF_RETRY_ON_EMPTY, default=False): cv.boolean, vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] ), diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index cfda4a3863a..7074431b0a9 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -35,6 +35,8 @@ CONF_PARITY = "parity" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" CONF_REGISTERS = "registers" +CONF_RETRIES = "retries" +CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" CONF_SCALE = "scale" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index f75c30b363b..ce4ea8235b4 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -39,6 +39,8 @@ from .const import ( CONF_BYTESIZE, CONF_CLOSE_COMM_ON_ERROR, CONF_PARITY, + CONF_RETRIES, + CONF_RETRY_ON_EMPTY, CONF_STOPBITS, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, @@ -149,6 +151,8 @@ class ModbusHub: self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = client_config[CONF_DELAY] self._config_reset_socket = client_config[CONF_CLOSE_COMM_ON_ERROR] + self._config_retries = client_config[CONF_RETRIES] + self._config_retry_on_empty = client_config[CONF_RETRY_ON_EMPTY] Defaults.Timeout = client_config[CONF_TIMEOUT] if self._config_type == "serial": # serial configuration @@ -216,7 +220,8 @@ class ModbusHub: bytesize=self._config_bytesize, parity=self._config_parity, timeout=self._config_timeout, - retry_on_empty=True, + retries=self._config_retries, + retry_on_empty=self._config_retry_on_empty, reset_socket=self._config_reset_socket, ) elif self._config_type == "rtuovertcp": @@ -225,6 +230,8 @@ class ModbusHub: port=self._config_port, framer=ModbusRtuFramer, timeout=self._config_timeout, + retries=self._config_retries, + retry_on_empty=self._config_retry_on_empty, reset_socket=self._config_reset_socket, ) elif self._config_type == "tcp": @@ -232,6 +239,8 @@ class ModbusHub: host=self._config_host, port=self._config_port, timeout=self._config_timeout, + retries=self._config_retries, + retry_on_empty=self._config_retry_on_empty, reset_socket=self._config_reset_socket, ) elif self._config_type == "udp": @@ -239,6 +248,8 @@ class ModbusHub: host=self._config_host, port=self._config_port, timeout=self._config_timeout, + retries=self._config_retries, + retry_on_empty=self._config_retry_on_empty, reset_socket=self._config_reset_socket, ) except ModbusException as exception_error: From e560e623e9c276d548129ac01fcb4b0f2d6b2e58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 6 Jun 2021 11:13:18 +0200 Subject: [PATCH 198/750] Add color_mode white (#51411) * Add color_mode white * Include brightness in white parameter * Reformat * Improve test coverage --- homeassistant/components/light/__init__.py | 43 ++++++---- .../components/light/reproduce_state.py | 47 +++++++---- tests/components/light/test_init.py | 82 +++++++++++++++++++ .../components/light/test_reproduce_state.py | 35 ++++++-- 4 files changed, 168 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7c650c06cf8..32494592697 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -61,6 +61,7 @@ COLOR_MODE_XY = "xy" COLOR_MODE_RGB = "rgb" COLOR_MODE_RGBW = "rgbw" COLOR_MODE_RGBWW = "rgbww" +COLOR_MODE_WHITE = "white" # Must *NOT* be the only supported mode VALID_COLOR_MODES = { COLOR_MODE_ONOFF, @@ -71,6 +72,7 @@ VALID_COLOR_MODES = { COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, } COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} COLOR_MODES_COLOR = { @@ -90,6 +92,7 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]: or COLOR_MODE_UNKNOWN in color_modes or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1) or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1) + or (COLOR_MODE_WHITE in color_modes and len(color_modes) == 1) ): raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}") return color_modes @@ -151,6 +154,7 @@ ATTR_MIN_MIREDS = "min_mireds" ATTR_MAX_MIREDS = "max_mireds" ATTR_COLOR_NAME = "color_name" ATTR_WHITE_VALUE = "white_value" +ATTR_WHITE = "white" # Brightness of the light, 0..255 or percentage ATTR_BRIGHTNESS = "brightness" @@ -195,6 +199,19 @@ LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) ), @@ -207,19 +224,7 @@ LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - vol.Coerce(tuple), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: VALID_FLASH, ATTR_EFFECT: cv.string, @@ -268,7 +273,7 @@ def preprocess_turn_on_alternatives(hass, params): def filter_turn_off_params(light, params): - """Filter out params not used in turn off.""" + """Filter out params not used in turn off or not supported by the light.""" supported_features = light.supported_features if not supported_features & SUPPORT_FLASH: @@ -280,7 +285,7 @@ def filter_turn_off_params(light, params): def filter_turn_on_params(light, params): - """Filter out params not used in turn off.""" + """Filter out params not supported by the light.""" supported_features = light.supported_features if not supported_features & SUPPORT_EFFECT: @@ -307,6 +312,8 @@ def filter_turn_on_params(light, params): params.pop(ATTR_RGBW_COLOR, None) if COLOR_MODE_RGBWW not in supported_color_modes: params.pop(ATTR_RGBWW_COLOR, None) + if COLOR_MODE_WHITE not in supported_color_modes: + params.pop(ATTR_WHITE, None) if COLOR_MODE_XY not in supported_color_modes: params.pop(ATTR_XY_COLOR, None) @@ -427,11 +434,15 @@ async def async_setup(hass, config): # noqa: C901 *rgb_color, light.min_mireds, light.max_mireds ) + # If both white and brightness are specified, override white + if ATTR_WHITE in params and COLOR_MODE_WHITE in supported_color_modes: + params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE]) + # Remove deprecated white value if the light supports color mode if supported_color_modes: params.pop(ATTR_WHITE_VALUE, None) - if params.get(ATTR_BRIGHTNESS) == 0: + if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: await async_handle_light_off_service(light, call) else: await light.async_turn_on(**filter_turn_on_params(light, params)) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index fa70670eee7..77e5742bbab 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any +from typing import Any, cast from homeassistant.const import ( ATTR_ENTITY_ID, @@ -31,6 +31,7 @@ from . import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, ATTR_WHITE_VALUE, ATTR_XY_COLOR, COLOR_MODE_COLOR_TEMP, @@ -39,6 +40,7 @@ from . import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_UNKNOWN, + COLOR_MODE_WHITE, COLOR_MODE_XY, DOMAIN, ) @@ -70,12 +72,13 @@ COLOR_GROUP = [ ] COLOR_MODE_TO_ATTRIBUTE = { - COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP, - COLOR_MODE_HS: ATTR_HS_COLOR, - COLOR_MODE_RGB: ATTR_RGB_COLOR, - COLOR_MODE_RGBW: ATTR_RGBW_COLOR, - COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, - COLOR_MODE_XY: ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP: (ATTR_COLOR_TEMP, ATTR_COLOR_TEMP), + COLOR_MODE_HS: (ATTR_HS_COLOR, ATTR_HS_COLOR), + COLOR_MODE_RGB: (ATTR_RGB_COLOR, ATTR_RGB_COLOR), + COLOR_MODE_RGBW: (ATTR_RGBW_COLOR, ATTR_RGBW_COLOR), + COLOR_MODE_RGBWW: (ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR), + COLOR_MODE_WHITE: (ATTR_WHITE, ATTR_BRIGHTNESS), + COLOR_MODE_XY: (ATTR_XY_COLOR, ATTR_XY_COLOR), } DEPRECATED_GROUP = [ @@ -93,6 +96,17 @@ DEPRECATION_WARNING = ( ) +def _color_mode_same(cur_state: State, state: State) -> bool: + """Test if color_mode is same.""" + cur_color_mode = cur_state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN) + saved_color_mode = state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN) + + # Guard for scenes etc. which where created before color modes were introduced + if saved_color_mode == COLOR_MODE_UNKNOWN: + return True + return cast(bool, cur_color_mode == saved_color_mode) + + async def _async_reproduce_state( hass: HomeAssistant, state: State, @@ -119,9 +133,13 @@ async def _async_reproduce_state( _LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs) # Return if we are already at the right state. - if cur_state.state == state.state and all( - check_attr_equal(cur_state.attributes, state.attributes, attr) - for attr in ATTR_GROUP + COLOR_GROUP + if ( + cur_state.state == state.state + and _color_mode_same(cur_state, state) + and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + COLOR_GROUP + ) ): return @@ -144,16 +162,17 @@ async def _async_reproduce_state( # Remove deprecated white value if we got a valid color mode service_data.pop(ATTR_WHITE_VALUE, None) color_mode = state.attributes[ATTR_COLOR_MODE] - if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): - if color_attr not in state.attributes: + if parameter_state := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): + parameter, state_attr = parameter_state + if state_attr not in state.attributes: _LOGGER.warning( "Color mode %s specified but attribute %s missing for: %s", color_mode, - color_attr, + state_attr, state.entity_id, ) return - service_data[color_attr] = state.attributes[color_attr] + service_data[parameter] = state.attributes[state_attr] else: # Fall back to Choosing the first color that is specified for color_attr in COLOR_GROUP: diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 842fb305c6c..368a5b0dab4 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1586,6 +1586,88 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} +async def test_light_service_call_white_mode(hass, enable_custom_integrations): + """Test color_mode white in service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_white", STATE_ON)) + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS, light.COLOR_MODE_WHITE} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_WHITE, + ] + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [entity0.entity_id], + "brightness_pct": 100, + "hs_color": (240, 100), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} + + entity0.calls = [] + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id], "white": 50}, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"white": 50} + + entity0.calls = [] + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id], "white": 0}, + blocking=True, + ) + _, data = entity0.last_call("turn_off") + assert data == {} + + entity0.calls = [] + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id], "brightness_pct": 100, "white": 50}, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"white": 255} + + entity0.calls = [] + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id], "brightness": 100, "white": 0}, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"white": 100} + + entity0.calls = [] + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id], "brightness_pct": 0, "white": 50}, + blocking=True, + ) + _, data = entity0.last_call("turn_off") + assert data == {} + + async def test_light_state_color_conversion(hass, enable_custom_integrations): """Test color conversion in state updates.""" platform = getattr(hass.components, "test.light") diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 815b8831d37..97d969acdd9 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -172,6 +172,7 @@ async def test_reproducing_states(hass, caplog): light.COLOR_MODE_RGBW, light.COLOR_MODE_RGBWW, light.COLOR_MODE_UNKNOWN, + light.COLOR_MODE_WHITE, light.COLOR_MODE_XY, ), ) @@ -188,6 +189,7 @@ async def test_filter_color_modes(hass, caplog, color_mode): **VALID_RGBW_COLOR, **VALID_RGBWW_COLOR, **VALID_XY_COLOR, + **VALID_BRIGHTNESS, } turn_on_calls = async_mock_service(hass, "light", "turn_on") @@ -197,15 +199,23 @@ async def test_filter_color_modes(hass, caplog, color_mode): ) expected_map = { - light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP, - light.COLOR_MODE_BRIGHTNESS: {}, - light.COLOR_MODE_HS: VALID_HS_COLOR, - light.COLOR_MODE_ONOFF: {}, - light.COLOR_MODE_RGB: VALID_RGB_COLOR, - light.COLOR_MODE_RGBW: VALID_RGBW_COLOR, - light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR, - light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE}, - light.COLOR_MODE_XY: VALID_XY_COLOR, + light.COLOR_MODE_COLOR_TEMP: {**VALID_BRIGHTNESS, **VALID_COLOR_TEMP}, + light.COLOR_MODE_BRIGHTNESS: VALID_BRIGHTNESS, + light.COLOR_MODE_HS: {**VALID_BRIGHTNESS, **VALID_HS_COLOR}, + light.COLOR_MODE_ONOFF: {**VALID_BRIGHTNESS}, + light.COLOR_MODE_RGB: {**VALID_BRIGHTNESS, **VALID_RGB_COLOR}, + light.COLOR_MODE_RGBW: {**VALID_BRIGHTNESS, **VALID_RGBW_COLOR}, + light.COLOR_MODE_RGBWW: {**VALID_BRIGHTNESS, **VALID_RGBWW_COLOR}, + light.COLOR_MODE_UNKNOWN: { + **VALID_BRIGHTNESS, + **VALID_HS_COLOR, + **VALID_WHITE_VALUE, + }, + light.COLOR_MODE_WHITE: { + **VALID_BRIGHTNESS, + light.ATTR_WHITE: VALID_BRIGHTNESS[light.ATTR_BRIGHTNESS], + }, + light.COLOR_MODE_XY: {**VALID_BRIGHTNESS, **VALID_XY_COLOR}, } expected = expected_map[color_mode] @@ -213,6 +223,13 @@ async def test_filter_color_modes(hass, caplog, color_mode): assert turn_on_calls[0].domain == "light" assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected} + # This should do nothing, the light is already in the desired state + hass.states.async_set("light.entity", "on", {"color_mode": color_mode, **expected}) + await hass.helpers.state.async_reproduce_state( + [State("light.entity", "on", {**expected, "color_mode": color_mode})] + ) + assert len(turn_on_calls) == 1 + async def test_deprecation_warning(hass, caplog): """Test deprecation warning.""" From 5bbf0ca6abf3f0117b61ee5162418bcd8b6e0ab0 Mon Sep 17 00:00:00 2001 From: drinfernoo <2319508+drinfernoo@users.noreply.github.com> Date: Sun, 6 Jun 2021 03:13:35 -0700 Subject: [PATCH 199/750] Add workaround for missing cleaning time in roomba (#51163) --- .../components/roomba/irobot_base.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 45a69d38576..cd82f51b8e3 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -23,6 +23,7 @@ from homeassistant.components.vacuum import ( ) import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util from . import roomba_reported_state from .const import DOMAIN @@ -191,14 +192,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # currently on if self.state == STATE_CLEANING: # Get clean mission status - mission_state = state.get("cleanMissionStatus", {}) - cleaning_time = mission_state.get("mssnM") - cleaned_area = mission_state.get("sqft") # Imperial - # Convert to m2 if the unit_system is set to metric - if cleaned_area and self.hass.config.units.is_metric: - cleaned_area = round(cleaned_area * 0.0929) - state_attrs[ATTR_CLEANING_TIME] = cleaning_time - state_attrs[ATTR_CLEANED_AREA] = cleaned_area + ( + state_attrs[ATTR_CLEANING_TIME], + state_attrs[ATTR_CLEANED_AREA], + ) = self.get_cleaning_status(state) # Error if self.vacuum.error_code != 0: @@ -219,6 +216,25 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): return state_attrs + def get_cleaning_status(self, state) -> tuple[int, int]: + """Return the cleaning time and cleaned area from the device.""" + if not (mission_state := state.get("cleanMissionStatus")): + return (0, 0) + + if cleaning_time := mission_state.get("mssnM", 0): + pass + elif start_time := mission_state.get("mssnStrtTm"): + now = dt_util.as_timestamp(dt_util.utcnow()) + if now > start_time: + cleaning_time = (now - start_time) // 60 + + if cleaned_area := mission_state.get("sqft", 0): # Imperial + # Convert to m2 if the unit_system is set to metric + if self.hass.config.units.is_metric: + cleaned_area = round(cleaned_area * 0.0929) + + return (cleaning_time, cleaned_area) + def on_message(self, json_data): """Update state on message change.""" state = json_data.get("state", {}).get("reported", {}) From f221deef2d82ce5dfc38fa24bff22aafebeb70e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jun 2021 07:59:58 -1000 Subject: [PATCH 200/750] Ensure from __future__ import annotations in irobot_base (#51554) --- homeassistant/components/roomba/irobot_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index cd82f51b8e3..2f57ef954b6 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -1,4 +1,6 @@ """Base class for iRobot devices.""" +from __future__ import annotations + import asyncio import logging From fcb8ab23abb4326f56a8f058a12b1dc2aa900ddb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 7 Jun 2021 04:03:56 +1000 Subject: [PATCH 201/750] Improve log message when zone missing in geolocation trigger (#51522) * log warning message if zone cannot be found * improve log message * add test case --- .../components/geo_location/trigger.py | 11 +++++ tests/components/geo_location/test_trigger.py | 45 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 3cdefccfeab..4410d39c0a6 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -1,4 +1,6 @@ """Offer geolocation automation rules.""" +import logging + import voluptuous as vol from homeassistant.components.geo_location import DOMAIN @@ -10,6 +12,8 @@ from homeassistant.helpers.event import TrackStates, async_track_state_change_fi # mypy: allow-untyped-defs, no-check-untyped-defs +_LOGGER = logging.getLogger(__name__) + EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER @@ -49,6 +53,13 @@ async def async_attach_trigger(hass, config, action, automation_info): return zone_state = hass.states.get(zone_entity_id) + if zone_state is None: + _LOGGER.warning( + "Unable to execute automation %s: Zone %s not found", + automation_info["name"], + zone_entity_id, + ) + return from_match = ( condition.zone(hass, zone_state, from_state) if from_state else False diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index e40b134e657..bc74f01f6f1 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -1,4 +1,6 @@ """The tests for the geolocation trigger.""" +import logging + import pytest from homeassistant.components import automation, zone @@ -318,3 +320,46 @@ async def test_if_fires_on_zone_disappear(hass, calls): assert ( calls[0].data["some"] == "geo_location - geo_location.entity - hello - - test" ) + + +async def test_zone_undefined(hass, calls, caplog): + """Test for undefined zone.""" + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564, "source": "test_source"}, + ) + await hass.async_block_till_done() + + caplog.set_level(logging.WARNING) + + zone_does_not_exist = "zone.does_not_exist" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "geo_location", + "source": "test_source", + "zone": zone_does_not_exist, + "event": "leave", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.881011, "longitude": -117.234758, "source": "test_source"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + assert ( + f"Unable to execute automation automation 0: Zone {zone_does_not_exist} not found" + in caplog.text + ) From c43bdbf7c889acb472f364943e18066401b6aa43 Mon Sep 17 00:00:00 2001 From: Colin Robbins Date: Sun, 6 Jun 2021 19:10:16 +0100 Subject: [PATCH 202/750] Add lightwave state_class and unique_id properties (#51544) * Add state_class and unique_id properties * Update homeassistant/components/lightwave/sensor.py Co-authored-by: Martin Hjelmare * fix isort * set class via attribute Co-authored-by: Martin Hjelmare --- homeassistant/components/lightwave/climate.py | 1 + homeassistant/components/lightwave/sensor.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 0518e91dda9..44b1e29ff34 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -42,6 +42,7 @@ class LightwaveTrv(ClimateEntity): self._hvac_action = None self._lwlink = lwlink self._serial = serial + self._attr_unique_id = f"{serial}-trv" # inhibit is used to prevent race condition on update. If non zero, skip next update cycle. self._inhibit = 0 diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index f1b6412ab6a..b298b78c7f6 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -1,5 +1,5 @@ """Support for LightwaveRF TRV - Associated Battery.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE from . import CONF_SERIAL, LIGHTWAVE_LINK @@ -27,6 +27,7 @@ class LightwaveBattery(SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_unit_of_measurement = PERCENTAGE + _attr_state_class = STATE_CLASS_MEASUREMENT def __init__(self, name, lwlink, serial): """Initialize the Lightwave Trv battery sensor.""" @@ -34,6 +35,7 @@ class LightwaveBattery(SensorEntity): self._state = None self._lwlink = lwlink self._serial = serial + self._attr_unique_id = f"{serial}-trv-battery" @property def name(self): From 7615af35d8c81f960a5f1fa6b369f914272e3430 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 7 Jun 2021 00:18:16 +0000 Subject: [PATCH 203/750] [ci skip] Translation update --- .../components/airvisual/translations/he.json | 9 ++++++ .../ambiclimate/translations/he.json | 3 ++ .../components/apple_tv/translations/he.json | 10 ++++++ .../components/arcam_fmj/translations/he.json | 3 +- .../components/asuswrt/translations/he.json | 9 +++++- .../components/august/translations/he.json | 4 ++- .../components/aurora/translations/he.json | 5 +++ .../components/awair/translations/he.json | 3 +- .../components/axis/translations/he.json | 1 + .../azure_devops/translations/he.json | 7 ++++- .../components/bond/translations/he.json | 6 ++++ .../components/braviatv/translations/he.json | 5 +++ .../components/brother/translations/he.json | 1 + .../components/bsblan/translations/he.json | 3 ++ .../components/camera/translations/he.json | 2 +- .../components/canary/translations/he.json | 1 + .../components/climacell/translations/he.json | 1 + .../coolmaster/translations/he.json | 1 + .../coronavirus/translations/he.json | 8 +++++ .../components/deconz/translations/he.json | 9 ++++++ .../devolo_home_control/translations/he.json | 3 ++ .../components/dexcom/translations/he.json | 5 +++ .../components/eafm/translations/he.json | 3 ++ .../components/ecobee/translations/he.json | 3 ++ .../components/elgato/translations/he.json | 10 ++++-- .../components/emonitor/translations/he.json | 8 +++++ .../enphase_envoy/translations/he.json | 8 ++++- .../components/esphome/translations/he.json | 4 ++- .../components/ezviz/translations/he.json | 12 ++++++- .../components/flo/translations/he.json | 8 +++++ .../forked_daapd/translations/he.json | 3 ++ .../components/fritzbox/translations/he.json | 3 +- .../fritzbox_callmonitor/translations/he.json | 14 +++++++++ .../geonetnz_quakes/translations/he.json | 7 +++++ .../components/gios/translations/he.json | 10 ++++++ .../components/goalzero/translations/he.json | 7 +++-- .../google_travel_time/translations/he.json | 7 +++-- .../components/gpslogger/translations/he.json | 7 +++++ .../components/group/translations/he.json | 2 +- .../components/guardian/translations/he.json | 17 ++++++++++ .../components/heos/translations/he.json | 6 ++++ .../components/hive/translations/he.json | 1 + .../home_plus_control/translations/he.json | 5 +++ .../homekit_controller/translations/he.json | 8 +++++ .../huawei_lte/translations/he.json | 4 ++- .../components/hue/translations/he.json | 1 + .../huisbaasje/translations/he.json | 3 +- .../translations/he.json | 3 ++ .../hvv_departures/translations/he.json | 16 ++++++++++ .../components/hyperion/translations/he.json | 3 +- .../components/ialarm/translations/he.json | 10 +++++- .../components/iaqualink/translations/he.json | 6 ++++ .../components/icloud/translations/he.json | 4 +++ .../components/ifttt/translations/he.json | 7 +++++ .../components/ipma/translations/he.json | 1 + .../components/ipp/translations/he.json | 7 +++++ .../components/kmtronic/translations/he.json | 4 +++ .../components/kodi/translations/he.json | 8 +++++ .../components/locative/translations/he.json | 7 +++++ .../components/luftdaten/translations/he.json | 8 +++++ .../lutron_caseta/translations/he.json | 7 +++++ .../components/lyric/translations/he.json | 6 +++- .../components/mailgun/translations/he.json | 7 +++++ .../components/met/translations/he.json | 11 +++++-- .../met_eireann/translations/he.json | 18 +++++++++++ .../meteo_france/translations/he.json | 4 +++ .../components/metoffice/translations/he.json | 7 +++++ .../components/mikrotik/translations/he.json | 3 +- .../components/mqtt/translations/he.json | 4 +++ .../components/mysensors/translations/he.json | 4 ++- .../components/nest/translations/he.json | 1 + .../components/netatmo/translations/he.json | 3 ++ .../nightscout/translations/he.json | 3 ++ .../components/nut/translations/he.json | 6 ++++ .../components/nws/translations/he.json | 9 ++++++ .../components/nzbget/translations/he.json | 14 ++++++++- .../ondilo_ico/translations/he.json | 16 ++++++++++ .../components/onvif/translations/he.json | 31 ++++++++++++++++--- .../opentherm_gw/translations/he.json | 1 + .../openweathermap/translations/he.json | 4 +++ .../components/ozw/translations/he.json | 8 ++++- .../philips_js/translations/he.json | 7 ++++- .../components/plaato/translations/he.json | 9 +++++- .../components/plex/translations/he.json | 1 + .../components/plugwise/translations/he.json | 1 + .../components/powerwall/translations/he.json | 6 ++++ .../pvpc_hourly_pricing/translations/he.json | 7 +++++ .../rainmachine/translations/he.json | 3 ++ .../components/rfxtrx/translations/he.json | 9 ++++-- .../components/ring/translations/he.json | 4 +++ .../components/risco/translations/he.json | 9 ++++++ .../components/roku/translations/he.json | 7 ++++- .../components/roon/translations/he.json | 7 +++++ .../components/samsungtv/translations/he.json | 2 ++ .../components/script/translations/he.json | 2 +- .../components/sharkiq/translations/he.json | 2 ++ .../shopping_list/translations/he.json | 3 ++ .../simplisafe/translations/he.json | 6 +++- .../components/sma/translations/he.json | 15 ++++++++- .../components/smarthab/translations/he.json | 1 + .../components/solaredge/translations/he.json | 7 +++++ .../components/solarlog/translations/he.json | 1 + .../components/soma/translations/he.json | 3 +- .../components/somfy/translations/he.json | 5 +++ .../components/sonarr/translations/he.json | 4 +++ .../speedtestdotnet/translations/he.json | 12 +++++++ .../components/subaru/translations/he.json | 5 +++ .../components/tado/translations/he.json | 5 +++ .../components/tesla/translations/he.json | 7 +++++ .../components/toon/translations/he.json | 3 +- .../totalconnect/translations/he.json | 3 ++ .../components/traccar/translations/he.json | 8 +++++ .../components/tradfri/translations/he.json | 3 +- .../transmission/translations/he.json | 3 +- .../components/tuya/translations/he.json | 10 +++++- .../components/twilio/translations/he.json | 4 +++ .../components/upcloud/translations/he.json | 4 +++ .../components/velbus/translations/he.json | 11 +++++++ .../components/vilfo/translations/he.json | 5 +++ .../components/vizio/translations/he.json | 4 ++- .../components/volumio/translations/he.json | 3 ++ .../water_heater/translations/he.json | 8 ++++- .../waze_travel_time/translations/he.json | 21 ++++++++++++- .../components/wemo/translations/he.json | 8 +++++ .../components/wiffi/translations/he.json | 11 +++++++ .../components/withings/translations/he.json | 3 +- .../components/wled/translations/he.json | 7 ++++- .../xiaomi_miio/translations/he.json | 10 ++++++ 128 files changed, 740 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/coronavirus/translations/he.json create mode 100644 homeassistant/components/geonetnz_quakes/translations/he.json create mode 100644 homeassistant/components/gpslogger/translations/he.json create mode 100644 homeassistant/components/guardian/translations/he.json create mode 100644 homeassistant/components/homekit_controller/translations/he.json create mode 100644 homeassistant/components/ifttt/translations/he.json create mode 100644 homeassistant/components/locative/translations/he.json create mode 100644 homeassistant/components/luftdaten/translations/he.json create mode 100644 homeassistant/components/mailgun/translations/he.json create mode 100644 homeassistant/components/met_eireann/translations/he.json create mode 100644 homeassistant/components/ondilo_ico/translations/he.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/he.json create mode 100644 homeassistant/components/speedtestdotnet/translations/he.json create mode 100644 homeassistant/components/traccar/translations/he.json create mode 100644 homeassistant/components/velbus/translations/he.json create mode 100644 homeassistant/components/wemo/translations/he.json create mode 100644 homeassistant/components/wiffi/translations/he.json diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index b6e4a181168..a5d64ed3a86 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7", "location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" @@ -22,6 +26,11 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, "description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4." + }, + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } } } } diff --git a/homeassistant/components/ambiclimate/translations/he.json b/homeassistant/components/ambiclimate/translations/he.json index 8b4db32d23a..7b7ec9c8c30 100644 --- a/homeassistant/components/ambiclimate/translations/he.json +++ b/homeassistant/components/ambiclimate/translations/he.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index faef37945c1..7173d6715e8 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "flow_title": "{name}", "step": { "pair_with_pin": { "data": { diff --git a/homeassistant/components/arcam_fmj/translations/he.json b/homeassistant/components/arcam_fmj/translations/he.json index afd05f99cac..0a4bd9ca12a 100644 --- a/homeassistant/components/arcam_fmj/translations/he.json +++ b/homeassistant/components/arcam_fmj/translations/he.json @@ -12,7 +12,8 @@ }, "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" }, "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df." } diff --git a/homeassistant/components/asuswrt/translations/he.json b/homeassistant/components/asuswrt/translations/he.json index def73c48be3..7b859e5af0c 100644 --- a/homeassistant/components/asuswrt/translations/he.json +++ b/homeassistant/components/asuswrt/translations/he.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", "pwd_and_ssh": "\u05e1\u05e4\u05e7 \u05e8\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH", - "pwd_or_ssh": "\u05d0\u05e0\u05d0 \u05e1\u05e4\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH" + "pwd_or_ssh": "\u05d0\u05e0\u05d0 \u05e1\u05e4\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", + "mode": "\u05de\u05e6\u05d1", + "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05ea\u05d7\u05d4", "ssh_key": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc SSH (\u05d1\u05de\u05e7\u05d5\u05dd \u05dc\u05e1\u05d9\u05e1\u05de\u05d4)", diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json index 4462aa91cf7..5fb689b2562 100644 --- a/homeassistant/components/august/translations/he.json +++ b/homeassistant/components/august/translations/he.json @@ -14,10 +14,12 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} ." + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username} .", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d7\u05e9\u05d1\u05d5\u05df August" }, "user_validate": { "data": { + "login_method": "\u05e9\u05d9\u05d8\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/aurora/translations/he.json b/homeassistant/components/aurora/translations/he.json index a7a3d83cfa2..a11e0a72254 100644 --- a/homeassistant/components/aurora/translations/he.json +++ b/homeassistant/components/aurora/translations/he.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" } } diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index f1c920db1cb..55e8b21a52b 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -12,7 +12,8 @@ "step": { "reauth": { "data": { - "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", + "email": "\u05d3\u05d5\u05d0\"\u05dc" } }, "user": { diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json index 1c9a7404efd..c0921a88332 100644 --- a/homeassistant/components/axis/translations/he.json +++ b/homeassistant/components/axis/translations/he.json @@ -1,6 +1,7 @@ { "config": { "error": { + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, diff --git a/homeassistant/components/azure_devops/translations/he.json b/homeassistant/components/azure_devops/translations/he.json index 3409550386c..7dc65806162 100644 --- a/homeassistant/components/azure_devops/translations/he.json +++ b/homeassistant/components/azure_devops/translations/he.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" - } + }, + "flow_title": "{project_url}" } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/he.json b/homeassistant/components/bond/translations/he.json index 3d38d2deaac..1cbd27129b2 100644 --- a/homeassistant/components/bond/translations/he.json +++ b/homeassistant/components/bond/translations/he.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "flow_title": "{name} ({host})", "step": { "confirm": { @@ -12,6 +17,7 @@ }, "user": { "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", "host": "\u05de\u05d0\u05e8\u05d7" } } diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index 280986d15ca..ab9d638a8ac 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -8,6 +8,11 @@ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" }, "step": { + "authorize": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json index 9f7a1b049b3..af3f5750ddb 100644 --- a/homeassistant/components/brother/translations/he.json +++ b/homeassistant/components/brother/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "wrong_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd." }, "flow_title": "{model} {serial_number}", diff --git a/homeassistant/components/bsblan/translations/he.json b/homeassistant/components/bsblan/translations/he.json index 0e121bea5aa..2c183d1ac24 100644 --- a/homeassistant/components/bsblan/translations/he.json +++ b/homeassistant/components/bsblan/translations/he.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/camera/translations/he.json b/homeassistant/components/camera/translations/he.json index ccca3a79099..ca6b207762d 100644 --- a/homeassistant/components/camera/translations/he.json +++ b/homeassistant/components/camera/translations/he.json @@ -6,5 +6,5 @@ "streaming": "\u05de\u05d6\u05e8\u05d9\u05dd" } }, - "title": "\u05de\u05b7\u05e6\u05dc\u05b5\u05de\u05b8\u05d4" + "title": "\u05de\u05e6\u05dc\u05de\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/canary/translations/he.json b/homeassistant/components/canary/translations/he.json index 9de2fb80888..fbea60c3704 100644 --- a/homeassistant/components/canary/translations/he.json +++ b/homeassistant/components/canary/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/climacell/translations/he.json index a29131a61d4..b663a5e0a0f 100644 --- a/homeassistant/components/climacell/translations/he.json +++ b/homeassistant/components/climacell/translations/he.json @@ -9,6 +9,7 @@ "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "api_version": "\u05d2\u05e8\u05e1\u05ea API", "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" diff --git a/homeassistant/components/coolmaster/translations/he.json b/homeassistant/components/coolmaster/translations/he.json index a109558689a..5903faf3c72 100644 --- a/homeassistant/components/coolmaster/translations/he.json +++ b/homeassistant/components/coolmaster/translations/he.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "no_units": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d9\u05d7\u05d9\u05d3\u05d5\u05ea HVAC \u05d1\u05de\u05d0\u05e8\u05d7 CoolMasterNet." }, "step": { diff --git a/homeassistant/components/coronavirus/translations/he.json b/homeassistant/components/coronavirus/translations/he.json new file mode 100644 index 00000000000..f662edefc41 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 434cb12dcb3..74a8e1ba54b 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ", "updated_instance": "\u05de\u05d5\u05e4\u05e2 deCONZ \u05e2\u05d5\u05d3\u05db\u05df \u05e2\u05dd \u05db\u05ea\u05d5\u05d1\u05ea \u05de\u05d0\u05e8\u05d7\u05ea \u05d7\u05d3\u05e9\u05d4" }, @@ -26,5 +27,13 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9", + "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9", + "button_7": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d1\u05d9\u05e2\u05d9", + "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\u05d9" + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index f9936c17225..27b258cd756 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dexcom/translations/he.json b/homeassistant/components/dexcom/translations/he.json index 837c2ac983b..454b7e1ae51 100644 --- a/homeassistant/components/dexcom/translations/he.json +++ b/homeassistant/components/dexcom/translations/he.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/eafm/translations/he.json b/homeassistant/components/eafm/translations/he.json index ded87729985..d32dde2f055 100644 --- a/homeassistant/components/eafm/translations/he.json +++ b/homeassistant/components/eafm/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ecobee/translations/he.json b/homeassistant/components/ecobee/translations/he.json index 3780025080f..bafbfda24e8 100644 --- a/homeassistant/components/ecobee/translations/he.json +++ b/homeassistant/components/ecobee/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/he.json b/homeassistant/components/elgato/translations/he.json index 887a102c99a..e0d0e50be74 100644 --- a/homeassistant/components/elgato/translations/he.json +++ b/homeassistant/components/elgato/translations/he.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{serial_number}", "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/emonitor/translations/he.json b/homeassistant/components/emonitor/translations/he.json index df463fa852c..4ec15aa12cb 100644 --- a/homeassistant/components/emonitor/translations/he.json +++ b/homeassistant/components/emonitor/translations/he.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {model} ({host})?" diff --git a/homeassistant/components/enphase_envoy/translations/he.json b/homeassistant/components/enphase_envoy/translations/he.json index e2a84c257f3..94741f81ff9 100644 --- a/homeassistant/components/enphase_envoy/translations/he.json +++ b/homeassistant/components/enphase_envoy/translations/he.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{serial} ({host})", "step": { diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json index 5518029a18a..5c0f832ba4c 100644 --- a/homeassistant/components/esphome/translations/he.json +++ b/homeassistant/components/esphome/translations/he.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/ezviz/translations/he.json b/homeassistant/components/ezviz/translations/he.json index 56e85ef9641..e45d7b58600 100644 --- a/homeassistant/components/ezviz/translations/he.json +++ b/homeassistant/components/ezviz/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured_account": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured_account": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -31,5 +32,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d1\u05e7\u05e9\u05d4 (\u05e9\u05e0\u05d9\u05d5\u05ea)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/flo/translations/he.json b/homeassistant/components/flo/translations/he.json index bb4fc738217..479d2f2f5e8 100644 --- a/homeassistant/components/flo/translations/he.json +++ b/homeassistant/components/flo/translations/he.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/he.json b/homeassistant/components/forked_daapd/translations/he.json index 3b4e6e27cb3..31c52a3faad 100644 --- a/homeassistant/components/forked_daapd/translations/he.json +++ b/homeassistant/components/forked_daapd/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "wrong_host_or_port": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8. \u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d5\u05d0\u05ea \u05d4\u05d9\u05e6\u05d9\u05d0\u05d4.", "wrong_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4." diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json index 23be2761041..ec9248b5ea6 100644 --- a/homeassistant/components/fritzbox/translations/he.json +++ b/homeassistant/components/fritzbox/translations/he.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/he.json b/homeassistant/components/fritzbox_callmonitor/translations/he.json index bb4fc738217..7951a71054c 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/he.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/he.json @@ -1,10 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "{name}", "step": { + "phonebook": { + "data": { + "phonebook": "\u05e1\u05e4\u05e8 \u05d8\u05dc\u05e4\u05d5\u05e0\u05d9\u05dd" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/geonetnz_quakes/translations/he.json b/homeassistant/components/geonetnz_quakes/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/he.json b/homeassistant/components/gios/translations/he.json index 59a1fbe0eed..5bae816fc44 100644 --- a/homeassistant/components/gios/translations/he.json +++ b/homeassistant/components/gios/translations/he.json @@ -2,6 +2,16 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/he.json b/homeassistant/components/goalzero/translations/he.json index 2e549971b52..f645dcab778 100644 --- a/homeassistant/components/goalzero/translations/he.json +++ b/homeassistant/components/goalzero/translations/he.json @@ -6,12 +6,15 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json index ba750745154..0db92654b41 100644 --- a/homeassistant/components/google_travel_time/translations/he.json +++ b/homeassistant/components/google_travel_time/translations/he.json @@ -21,9 +21,12 @@ "step": { "init": { "data": { - "language": "\u05e9\u05e4\u05d4" + "language": "\u05e9\u05e4\u05d4", + "time": "\u05d6\u05de\u05df", + "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea" } } } - } + }, + "title": "\u05d6\u05de\u05df \u05e0\u05e1\u05d9\u05e2\u05d4 \u05d1\u05d2\u05d5\u05d2\u05dc \u05de\u05e4\u05d5\u05ea" } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/he.json b/homeassistant/components/gpslogger/translations/he.json new file mode 100644 index 00000000000..803c0e63ec3 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index caa6ee98ea8..be7c2657e9f 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -13,5 +13,5 @@ "unlocked": "\u05e4\u05ea\u05d5\u05d7" } }, - "title": "\u05e7\u05b0\u05d1\u05d5\u05bc\u05e6\u05b8\u05d4" + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/he.json b/homeassistant/components/guardian/translations/he.json new file mode 100644 index 00000000000..8d77b3cd257 --- /dev/null +++ b/homeassistant/components/guardian/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/he.json b/homeassistant/components/heos/translations/he.json index 92b9c1c48b2..2f2863169db 100644 --- a/homeassistant/components/heos/translations/he.json +++ b/homeassistant/components/heos/translations/he.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hive/translations/he.json b/homeassistant/components/hive/translations/he.json index 3552485d234..dcc967c6986 100644 --- a/homeassistant/components/hive/translations/he.json +++ b/homeassistant/components/hive/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { diff --git a/homeassistant/components/home_plus_control/translations/he.json b/homeassistant/components/home_plus_control/translations/he.json index f39370d6cb6..2800ddd7e62 100644 --- a/homeassistant/components/home_plus_control/translations/he.json +++ b/homeassistant/components/home_plus_control/translations/he.json @@ -1,6 +1,11 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "create_entry": { diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json new file mode 100644 index 00000000000..1028351a1bc --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json index 9b1aa61ecd2..f55b325d867 100644 --- a/homeassistant/components/huawei_lte/translations/he.json +++ b/homeassistant/components/huawei_lte/translations/he.json @@ -6,7 +6,9 @@ }, "error": { "incorrect_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4", - "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9" + "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/hue/translations/he.json b/homeassistant/components/hue/translations/he.json index 1621e947d68..c014b0a52ae 100644 --- a/homeassistant/components/hue/translations/he.json +++ b/homeassistant/components/hue/translations/he.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "\u05db\u05dc \u05d4\u05de\u05d2\u05e9\u05e8\u05d9\u05dd \u05e9\u05dc Philips Hue \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd \u05db\u05d1\u05e8", "already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05d2\u05e9\u05e8", "discover_timeout": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05d2\u05dc\u05d5\u05ea \u05de\u05d2\u05e9\u05e8\u05d9\u05dd", "no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 Philips Hue", diff --git a/homeassistant/components/huisbaasje/translations/he.json b/homeassistant/components/huisbaasje/translations/he.json index d1411d63b8d..c479d8488f2 100644 --- a/homeassistant/components/huisbaasje/translations/he.json +++ b/homeassistant/components/huisbaasje/translations/he.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/he.json b/homeassistant/components/hunterdouglas_powerview/translations/he.json index 87b0a5d8f50..c6610f79e77 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/he.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" diff --git a/homeassistant/components/hvv_departures/translations/he.json b/homeassistant/components/hvv_departures/translations/he.json index bb4fc738217..19463e18610 100644 --- a/homeassistant/components/hvv_departures/translations/he.json +++ b/homeassistant/components/hvv_departures/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { @@ -9,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "offset": "\u05d4\u05d9\u05e1\u05d8 (\u05d3\u05e7\u05d5\u05ea)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/he.json b/homeassistant/components/hyperion/translations/he.json index 7dafc7565e5..dd22953025f 100644 --- a/homeassistant/components/hyperion/translations/he.json +++ b/homeassistant/components/hyperion/translations/he.json @@ -16,7 +16,8 @@ }, "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/ialarm/translations/he.json b/homeassistant/components/ialarm/translations/he.json index b2a3b1b660e..58521f503e2 100644 --- a/homeassistant/components/ialarm/translations/he.json +++ b/homeassistant/components/ialarm/translations/he.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/iaqualink/translations/he.json b/homeassistant/components/iaqualink/translations/he.json index 2ec9b1b240b..d704c432c35 100644 --- a/homeassistant/components/iaqualink/translations/he.json +++ b/homeassistant/components/iaqualink/translations/he.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index 3320e8f3dcc..73f09385a36 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/ifttt/translations/he.json b/homeassistant/components/ifttt/translations/he.json new file mode 100644 index 00000000000..803c0e63ec3 --- /dev/null +++ b/homeassistant/components/ifttt/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/he.json b/homeassistant/components/ipma/translations/he.json index c4818393edd..90ccdbdd48f 100644 --- a/homeassistant/components/ipma/translations/he.json +++ b/homeassistant/components/ipma/translations/he.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "mode": "\u05de\u05e6\u05d1", "name": "\u05e9\u05dd" }, "title": "\u05de\u05d9\u05e7\u05d5\u05dd" diff --git a/homeassistant/components/ipp/translations/he.json b/homeassistant/components/ipp/translations/he.json index 7acc23c09e9..df531a51b69 100644 --- a/homeassistant/components/ipp/translations/he.json +++ b/homeassistant/components/ipp/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/kmtronic/translations/he.json b/homeassistant/components/kmtronic/translations/he.json index 7b48912f1b4..479d2f2f5e8 100644 --- a/homeassistant/components/kmtronic/translations/he.json +++ b/homeassistant/components/kmtronic/translations/he.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json index 89b6a0c30b9..162f1f86433 100644 --- a/homeassistant/components/kodi/translations/he.json +++ b/homeassistant/components/kodi/translations/he.json @@ -1,9 +1,17 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/locative/translations/he.json b/homeassistant/components/locative/translations/he.json new file mode 100644 index 00000000000..803c0e63ec3 --- /dev/null +++ b/homeassistant/components/locative/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/he.json b/homeassistant/components/luftdaten/translations/he.json new file mode 100644 index 00000000000..11a4c93b42a --- /dev/null +++ b/homeassistant/components/luftdaten/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json index 163a150d00c..cb742b61b72 100644 --- a/homeassistant/components/lutron_caseta/translations/he.json +++ b/homeassistant/components/lutron_caseta/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "flow_title": "{name} ({host})", "step": { "import_failed": { diff --git a/homeassistant/components/lyric/translations/he.json b/homeassistant/components/lyric/translations/he.json index be83e5f2fed..63428decee3 100644 --- a/homeassistant/components/lyric/translations/he.json +++ b/homeassistant/components/lyric/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", - "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/mailgun/translations/he.json b/homeassistant/components/mailgun/translations/he.json new file mode 100644 index 00000000000..803c0e63ec3 --- /dev/null +++ b/homeassistant/components/mailgun/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/he.json b/homeassistant/components/met/translations/he.json index 4c49313d977..f493f06b23c 100644 --- a/homeassistant/components/met/translations/he.json +++ b/homeassistant/components/met/translations/he.json @@ -1,10 +1,17 @@ { "config": { + "error": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, "step": { "user": { "data": { - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" - } + "elevation": "\u05d2\u05d5\u05d1\u05d4", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + }, + "title": "\u05de\u05d9\u05e7\u05d5\u05dd" } } } diff --git a/homeassistant/components/met_eireann/translations/he.json b/homeassistant/components/met_eireann/translations/he.json new file mode 100644 index 00000000000..f493f06b23c --- /dev/null +++ b/homeassistant/components/met_eireann/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "step": { + "user": { + "data": { + "elevation": "\u05d2\u05d5\u05d1\u05d4", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + }, + "title": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/he.json b/homeassistant/components/meteo_france/translations/he.json index 7055f451412..9b05c651e27 100644 --- a/homeassistant/components/meteo_france/translations/he.json +++ b/homeassistant/components/meteo_france/translations/he.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "cities": { "data": { diff --git a/homeassistant/components/metoffice/translations/he.json b/homeassistant/components/metoffice/translations/he.json index eb9c1a2a7ca..3ba18c5b4ab 100644 --- a/homeassistant/components/metoffice/translations/he.json +++ b/homeassistant/components/metoffice/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json index c4578e1c322..6f8286290d4 100644 --- a/homeassistant/components/mikrotik/translations/he.json +++ b/homeassistant/components/mikrotik/translations/he.json @@ -4,7 +4,8 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { "user": { diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 0b809b8d18e..ef628fb799b 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -20,10 +20,14 @@ } }, "options": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "broker": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da" diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index e9366a4e2ef..7ded555edb8 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 8b44c715ede..91274d8b731 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -3,6 +3,7 @@ "abort": { "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, diff --git a/homeassistant/components/netatmo/translations/he.json b/homeassistant/components/netatmo/translations/he.json index 85fd7994cf7..814a0093b2c 100644 --- a/homeassistant/components/netatmo/translations/he.json +++ b/homeassistant/components/netatmo/translations/he.json @@ -6,6 +6,9 @@ "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, "step": { "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" diff --git a/homeassistant/components/nightscout/translations/he.json b/homeassistant/components/nightscout/translations/he.json index c39016d3bb6..eebb1cc0a93 100644 --- a/homeassistant/components/nightscout/translations/he.json +++ b/homeassistant/components/nightscout/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json index b3fb785d55e..77c12630351 100644 --- a/homeassistant/components/nut/translations/he.json +++ b/homeassistant/components/nut/translations/he.json @@ -17,5 +17,11 @@ } } } + }, + "options": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } } } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/he.json b/homeassistant/components/nws/translations/he.json index 4c49313d977..3ba18c5b4ab 100644 --- a/homeassistant/components/nws/translations/he.json +++ b/homeassistant/components/nws/translations/he.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" } } diff --git a/homeassistant/components/nzbget/translations/he.json b/homeassistant/components/nzbget/translations/he.json index bb4fc738217..f1a32be9a5c 100644 --- a/homeassistant/components/nzbget/translations/he.json +++ b/homeassistant/components/nzbget/translations/he.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", "step": { "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } } diff --git a/homeassistant/components/ondilo_ico/translations/he.json b/homeassistant/components/ondilo_ico/translations/he.json new file mode 100644 index 00000000000..be83e5f2fed --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index b0ea84c04ba..ec9da5b556e 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -2,7 +2,12 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05dc\u05da.", + "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "auth": { @@ -14,19 +19,37 @@ "configure_profile": { "data": { "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" - } + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e4\u05e8\u05d5\u05e4\u05d9\u05dc\u05d9\u05dd" }, "device": { "data": { "host": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4" - } + }, + "title": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF" }, "manual_input": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", "name": "\u05e9\u05dd", "port": "\u05e4\u05d5\u05e8\u05d8" - } + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df ONVIF" + }, + "user": { + "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05de\u05db\u05e9\u05d9\u05e8\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u05d0\u05e8\u05d2\u05d5\u05de\u05e0\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd \u05e9\u05dc FFMPEG", + "rtsp_transport": "\u05de\u05e0\u05d2\u05e0\u05d5\u05df \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP" + }, + "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d4\u05ea\u05e7\u05df ONVIF" } } } diff --git a/homeassistant/components/opentherm_gw/translations/he.json b/homeassistant/components/opentherm_gw/translations/he.json index 2e4c2bd6cc2..eddeffa2ed0 100644 --- a/homeassistant/components/opentherm_gw/translations/he.json +++ b/homeassistant/components/opentherm_gw/translations/he.json @@ -2,6 +2,7 @@ "config": { "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" }, "step": { diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json index 920be9308a5..554fefee321 100644 --- a/homeassistant/components/openweathermap/translations/he.json +++ b/homeassistant/components/openweathermap/translations/he.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json index 5d8373f0eb0..10f3cb6d722 100644 --- a/homeassistant/components/ozw/translations/he.json +++ b/homeassistant/components/ozw/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { "on_supervisor": { @@ -11,6 +12,11 @@ }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 OpenZWave?", "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" + }, + "start_addon": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } } } } diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json index e4ecd0d0b9d..c860b365bff 100644 --- a/homeassistant/components/philips_js/translations/he.json +++ b/homeassistant/components/philips_js/translations/he.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "pairing_failure": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e9\u05d9\u05d9\u05da: {error_id}" }, "step": { "pair": { "data": { "pin": "\u05e7\u05d5\u05d3 PIN" - } + }, + "title": "\u05d6\u05d9\u05d5\u05d5\u05d2" }, "user": { "data": { diff --git a/homeassistant/components/plaato/translations/he.json b/homeassistant/components/plaato/translations/he.json index 937391f9327..014783d7431 100644 --- a/homeassistant/components/plaato/translations/he.json +++ b/homeassistant/components/plaato/translations/he.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/he.json b/homeassistant/components/plex/translations/he.json index 4c2a041ce41..dafda36af4c 100644 --- a/homeassistant/components/plex/translations/he.json +++ b/homeassistant/components/plex/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index 13cb7aea870..a38d2a24118 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -5,6 +5,7 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, + "flow_title": "{name}", "step": { "user_gateway": { "data": { diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json index 7f772fab5f9..f090e85c0cf 100644 --- a/homeassistant/components/powerwall/translations/he.json +++ b/homeassistant/components/powerwall/translations/he.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{ip_address}", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/he.json b/homeassistant/components/pvpc_hourly_pricing/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/he.json b/homeassistant/components/rainmachine/translations/he.json index fc24d9bfa43..0f8dcfb7b28 100644 --- a/homeassistant/components/rainmachine/translations/he.json +++ b/homeassistant/components/rainmachine/translations/he.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "flow_title": "{ip}", "step": { "user": { diff --git a/homeassistant/components/rfxtrx/translations/he.json b/homeassistant/components/rfxtrx/translations/he.json index 645424ac9af..cabe3734e11 100644 --- a/homeassistant/components/rfxtrx/translations/he.json +++ b/homeassistant/components/rfxtrx/translations/he.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "setup_network": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } }, "setup_serial": { diff --git a/homeassistant/components/ring/translations/he.json b/homeassistant/components/ring/translations/he.json index e428d0009ae..fe6357d0150 100644 --- a/homeassistant/components/ring/translations/he.json +++ b/homeassistant/components/ring/translations/he.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/risco/translations/he.json b/homeassistant/components/risco/translations/he.json index ed04412d69b..08c5ec7d4c0 100644 --- a/homeassistant/components/risco/translations/he.json +++ b/homeassistant/components/risco/translations/he.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "pin": "\u05e7\u05d5\u05d3 PIN", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json index 887a102c99a..651135370ba 100644 --- a/homeassistant/components/roku/translations/he.json +++ b/homeassistant/components/roku/translations/he.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/roon/translations/he.json b/homeassistant/components/roon/translations/he.json index 6df3cf0a9f9..9d3230d257e 100644 --- a/homeassistant/components/roon/translations/he.json +++ b/homeassistant/components/roon/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "link": { "description": "\u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e9\u05e8 \u05dc-Home Assistant \u05d1-Roon. \u05dc\u05d0\u05d7\u05e8 \u05e9\u05ea\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd Roon Core, \u05e4\u05ea\u05d7 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d5\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea HomeAssistant \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d4\u05e8\u05d7\u05d1\u05d5\u05ea." diff --git a/homeassistant/components/samsungtv/translations/he.json b/homeassistant/components/samsungtv/translations/he.json index 77a196b4ad9..9f62de51b8c 100644 --- a/homeassistant/components/samsungtv/translations/he.json +++ b/homeassistant/components/samsungtv/translations/he.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "auth_missing": "Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05d6\u05d5 \u05e9\u05dc Samsung. \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05d7\u05d9\u05e6\u05d5\u05e0\u05d9\u05d9\u05dd \u05e9\u05dc \u05d4\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d0\u05e9\u05e8 \u05d0\u05ea Home Assistant.", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, diff --git a/homeassistant/components/script/translations/he.json b/homeassistant/components/script/translations/he.json index f003c2e1210..7c2b808c58d 100644 --- a/homeassistant/components/script/translations/he.json +++ b/homeassistant/components/script/translations/he.json @@ -5,5 +5,5 @@ "on": "\u05d3\u05dc\u05d5\u05e7" } }, - "title": "\u05ea\u05b7\u05e1\u05e8\u05b4\u05d9\u05d8" + "title": "\u05e1\u05e7\u05e8\u05d9\u05e4\u05d8" } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/he.json b/homeassistant/components/sharkiq/translations/he.json index 624c820b177..684220fe307 100644 --- a/homeassistant/components/sharkiq/translations/he.json +++ b/homeassistant/components/sharkiq/translations/he.json @@ -5,6 +5,8 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/shopping_list/translations/he.json b/homeassistant/components/shopping_list/translations/he.json index 6c51baa2a56..f245ae2362b 100644 --- a/homeassistant/components/shopping_list/translations/he.json +++ b/homeassistant/components/shopping_list/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, "step": { "user": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05e7\u05d1\u05d5\u05e2 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05e7\u05e0\u05d9\u05d5\u05ea?", diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index f17c3879f5a..dd3969f269f 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/sma/translations/he.json b/homeassistant/components/sma/translations/he.json index ba081d16366..8ba7834c335 100644 --- a/homeassistant/components/sma/translations/he.json +++ b/homeassistant/components/sma/translations/he.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "cannot_retrieve_device_info": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4, \u05d0\u05da \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d4\u05ea\u05e7\u05df", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "group": "\u05e7\u05d1\u05d5\u05e6\u05d4", "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } } diff --git a/homeassistant/components/smarthab/translations/he.json b/homeassistant/components/smarthab/translations/he.json index 2aa9a81dcd6..c00515506ac 100644 --- a/homeassistant/components/smarthab/translations/he.json +++ b/homeassistant/components/smarthab/translations/he.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } diff --git a/homeassistant/components/solaredge/translations/he.json b/homeassistant/components/solaredge/translations/he.json index 3780025080f..e99a0a713b7 100644 --- a/homeassistant/components/solaredge/translations/he.json +++ b/homeassistant/components/solaredge/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/solarlog/translations/he.json b/homeassistant/components/solarlog/translations/he.json index 25fe66938d7..84abc77ba79 100644 --- a/homeassistant/components/solarlog/translations/he.json +++ b/homeassistant/components/solarlog/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { diff --git a/homeassistant/components/soma/translations/he.json b/homeassistant/components/soma/translations/he.json index b2a3b1b660e..cbb3cd76a9d 100644 --- a/homeassistant/components/soma/translations/he.json +++ b/homeassistant/components/soma/translations/he.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/somfy/translations/he.json b/homeassistant/components/somfy/translations/he.json index eef0bf816f4..c68d7f74d85 100644 --- a/homeassistant/components/somfy/translations/he.json +++ b/homeassistant/components/somfy/translations/he.json @@ -1,9 +1,14 @@ { "config": { "abort": { + "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, "step": { "pick_implementation": { "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json index 41569a46644..d033613184e 100644 --- a/homeassistant/components/sonarr/translations/he.json +++ b/homeassistant/components/sonarr/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -10,6 +11,9 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/speedtestdotnet/translations/he.json b/homeassistant/components/speedtestdotnet/translations/he.json new file mode 100644 index 00000000000..08506bf3437 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/he.json b/homeassistant/components/subaru/translations/he.json index 857993bee9d..51413724a5f 100644 --- a/homeassistant/components/subaru/translations/he.json +++ b/homeassistant/components/subaru/translations/he.json @@ -9,6 +9,11 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "pin": { + "data": { + "pin": "\u05e7\u05d5\u05d3 PIN" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/tado/translations/he.json b/homeassistant/components/tado/translations/he.json index a4d57c233d4..c479d8488f2 100644 --- a/homeassistant/components/tado/translations/he.json +++ b/homeassistant/components/tado/translations/he.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json index 1d26aa8eaf0..fe406357051 100644 --- a/homeassistant/components/tesla/translations/he.json +++ b/homeassistant/components/tesla/translations/he.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/toon/translations/he.json b/homeassistant/components/toon/translations/he.json index 967c4a9d7ce..431a7b32509 100644 --- a/homeassistant/components/toon/translations/he.json +++ b/homeassistant/components/toon/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", - "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json index 3c1c1fcbfda..712c12a1062 100644 --- a/homeassistant/components/totalconnect/translations/he.json +++ b/homeassistant/components/totalconnect/translations/he.json @@ -4,6 +4,9 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "locations": { "data": { diff --git a/homeassistant/components/traccar/translations/he.json b/homeassistant/components/traccar/translations/he.json new file mode 100644 index 00000000000..ebee9aee976 --- /dev/null +++ b/homeassistant/components/traccar/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/he.json b/homeassistant/components/tradfri/translations/he.json index 86006674421..800cb59a744 100644 --- a/homeassistant/components/tradfri/translations/he.json +++ b/homeassistant/components/tradfri/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index c4578e1c322..6f8286290d4 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -4,7 +4,8 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { "user": { diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 29ea89d824d..0a05bec6b21 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d8\u05d5\u05d9\u05d4", "step": { "user": { "data": { @@ -10,7 +16,9 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "platform": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05d1\u05d4 \u05e8\u05e9\u05d5\u05dd \u05d7\u05e9\u05d1\u05d5\u05e0\u05da", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.", + "title": "Tuya" } } }, diff --git a/homeassistant/components/twilio/translations/he.json b/homeassistant/components/twilio/translations/he.json index b109b127dec..7e155c6bdd7 100644 --- a/homeassistant/components/twilio/translations/he.json +++ b/homeassistant/components/twilio/translations/he.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + }, "step": { "user": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" diff --git a/homeassistant/components/upcloud/translations/he.json b/homeassistant/components/upcloud/translations/he.json index ac90b3264ea..b0279f2d508 100644 --- a/homeassistant/components/upcloud/translations/he.json +++ b/homeassistant/components/upcloud/translations/he.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/velbus/translations/he.json b/homeassistant/components/velbus/translations/he.json new file mode 100644 index 00000000000..c1b4500289b --- /dev/null +++ b/homeassistant/components/velbus/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/he.json b/homeassistant/components/vilfo/translations/he.json index 20b08543899..54110684daf 100644 --- a/homeassistant/components/vilfo/translations/he.json +++ b/homeassistant/components/vilfo/translations/he.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", "host": "\u05de\u05d0\u05e8\u05d7" }, "description": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e0\u05ea\u05d1 \u05e9\u05dc Vilfo. \u05d0\u05ea\u05d4 \u05e6\u05e8\u05d9\u05da \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7/IP \u05e9\u05dc \u05e0\u05ea\u05d1 Vilfo \u05e9\u05dc\u05da \u05d5\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05ea API. \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05e2\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05e7\u05d1\u05dc \u05e4\u05e8\u05d8\u05d9\u05dd \u05d0\u05dc\u05d4, \u05d1\u05e7\u05e8 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/vilfo" diff --git a/homeassistant/components/vizio/translations/he.json b/homeassistant/components/vizio/translations/he.json index 8d030e5662e..977db5ff9f3 100644 --- a/homeassistant/components/vizio/translations/he.json +++ b/homeassistant/components/vizio/translations/he.json @@ -15,7 +15,9 @@ }, "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/volumio/translations/he.json b/homeassistant/components/volumio/translations/he.json index 48c198c0a7f..58521f503e2 100644 --- a/homeassistant/components/volumio/translations/he.json +++ b/homeassistant/components/volumio/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" diff --git a/homeassistant/components/water_heater/translations/he.json b/homeassistant/components/water_heater/translations/he.json index 4b9d5bf4491..7978941f3bd 100644 --- a/homeassistant/components/water_heater/translations/he.json +++ b/homeassistant/components/water_heater/translations/he.json @@ -7,7 +7,13 @@ }, "state": { "_": { - "off": "\u05db\u05d1\u05d5\u05d9" + "eco": "\u05d7\u05e1\u05db\u05d5\u05e0\u05d9", + "electric": "\u05d7\u05e9\u05de\u05dc\u05d9", + "gas": "\u05d2\u05d6", + "heat_pump": "\u05de\u05e9\u05d0\u05d1\u05ea \u05d7\u05d5\u05dd", + "high_demand": "\u05d1\u05d9\u05e7\u05d5\u05e9 \u05d2\u05d1\u05d5\u05d4", + "off": "\u05db\u05d1\u05d5\u05d9", + "performance": "\u05d1\u05d9\u05e6\u05d5\u05e2\u05d9\u05dd" } } } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/he.json b/homeassistant/components/waze_travel_time/translations/he.json index a7a3d83cfa2..ab46aac243d 100644 --- a/homeassistant/components/waze_travel_time/translations/he.json +++ b/homeassistant/components/waze_travel_time/translations/he.json @@ -1,9 +1,28 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "user": { "data": { - "name": "\u05e9\u05dd" + "destination": "\u05d9\u05e2\u05d3", + "name": "\u05e9\u05dd", + "origin": "\u05de\u05e7\u05d5\u05e8", + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "realtime": "\u05d6\u05de\u05df \u05e0\u05e1\u05d9\u05e2\u05d4 \u05d1\u05d6\u05de\u05df \u05d0\u05de\u05ea?", + "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea" } } } diff --git a/homeassistant/components/wemo/translations/he.json b/homeassistant/components/wemo/translations/he.json new file mode 100644 index 00000000000..380dbc5d7fc --- /dev/null +++ b/homeassistant/components/wemo/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/he.json b/homeassistant/components/wiffi/translations/he.json new file mode 100644 index 00000000000..048132fe5b4 --- /dev/null +++ b/homeassistant/components/wiffi/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json index 971eda3486f..6bcd8bf9a9d 100644 --- a/homeassistant/components/withings/translations/he.json +++ b/homeassistant/components/withings/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", - "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3." + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" diff --git a/homeassistant/components/wled/translations/he.json b/homeassistant/components/wled/translations/he.json index 887a102c99a..1cd249b4daa 100644 --- a/homeassistant/components/wled/translations/he.json +++ b/homeassistant/components/wled/translations/he.json @@ -1,8 +1,13 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 2521b963485..79e2371c033 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -4,12 +4,22 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", "step": { "device": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" } + }, + "gateway": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } } } } From dc9b03154483f71ac0e01b70bb837d95ccd2302f Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 7 Jun 2021 10:04:03 +0200 Subject: [PATCH 204/750] Update pyhomematic to 0.1.73 (#51551) --- homeassistant/components/homematic/const.py | 5 +++++ homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 864441c2aa6..4f1c1d12f81 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -61,6 +61,7 @@ HM_DEVICE_TYPES = { "IOSwitchWireless", "IPWIODevice", "IPSwitchBattery", + "IPMultiIOPCB", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -122,6 +123,8 @@ HM_DEVICE_TYPES = { "IPKeyBlindTilt", "IPLanRouter", "TempModuleSTE2", + "IPMultiIOPCB", + "ValveBoxW", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -134,6 +137,7 @@ HM_DEVICE_TYPES = { "ThermostatGroup", "IPThermostatWall230V", "IPThermostatWall2", + "IPWThermostatWall", ], DISCOVER_BINARY_SENSORS: [ "ShutterContact", @@ -167,6 +171,7 @@ HM_DEVICE_TYPES = { "IPAlarmSensor", "IPRainSensor", "IPLanRouter", + "IPMultiIOPCB", ], DISCOVER_COVER: [ "Blind", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index ce192bc3808..8b1ee62a09e 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.72"], + "requirements": ["pyhomematic==0.1.73"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 0f466a7a55f..28ba647d47a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1457,7 +1457,7 @@ pyhik==0.2.8 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.72 +pyhomematic==0.1.73 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25bda050a52..98a3bbc5621 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -804,7 +804,7 @@ pyheos==0.7.2 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.72 +pyhomematic==0.1.73 # homeassistant.components.ialarm pyialarm==1.8.1 From b171c5ebe9ef4b70b56acab2a1a929222f0c4278 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 7 Jun 2021 10:09:08 +0200 Subject: [PATCH 205/750] Fix garmin_connect config flow multiple account creation (#51542) --- .../components/garmin_connect/config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 8e26e2bf608..8f83a9e1071 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -39,14 +39,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._show_setup_form() websession = async_get_clientsession(self.hass) + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] - garmin_client = Garmin( - websession, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] - ) + garmin_client = Garmin(websession, username, password) errors = {} try: - username = await garmin_client.login() + await garmin_client.login() except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) @@ -68,7 +68,7 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=username, data={ CONF_ID: username, - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_USERNAME: username, + CONF_PASSWORD: password, }, ) From ae83191121dfd3b8b7e7a6c17adeba4cc5435bed Mon Sep 17 00:00:00 2001 From: stephan192 Date: Mon, 7 Jun 2021 10:53:36 +0200 Subject: [PATCH 206/750] Bump dwdwfsapi to 1.0.4 (#51556) --- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index 1550d9262a4..55c848ea219 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -3,6 +3,6 @@ "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], - "requirements": ["dwdwfsapi==1.0.3"], + "requirements": ["dwdwfsapi==1.0.4"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 28ba647d47a..a21fb12143a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ dovado==0.4.1 dsmr_parser==0.29 # homeassistant.components.dwd_weather_warnings -dwdwfsapi==1.0.3 +dwdwfsapi==1.0.4 # homeassistant.components.dweet dweepy==0.3.0 From 85ce679c64d3c96e8c2fdbc8b9e3bfd0beb73b83 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 04:46:56 -0500 Subject: [PATCH 207/750] Fix Sonos restore calls (#51565) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 3932b6d3364..26a75f94065 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -765,7 +765,7 @@ class SonosSpeaker: """Pause all current coordinators and restore groups.""" for speaker in (s for s in speakers if s.is_coordinator): if speaker.media.playback_status == SONOS_STATE_PLAYING: - hass.async_create_task(speaker.soco.pause()) + speaker.soco.pause() groups = [] From 75dffee312c0b0ed93f6312d2005efbccc45bdc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jun 2021 23:49:37 -1000 Subject: [PATCH 208/750] Increase isy setup timeout to 60s (#51559) - Ensure errors are displayed in the UI --- homeassistant/components/isy994/__init__.py | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 99905e4d946..51c34aeb0a7 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -172,14 +172,12 @@ async def async_setup_entry( ) try: - async with async_timeout.timeout(30): + async with async_timeout.timeout(60): await isy.initialize() except asyncio.TimeoutError as err: - _LOGGER.error( - "Timed out initializing the ISY; device may be busy, trying again later: %s", - err, - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Timed out initializing the ISY; device may be busy, trying again later: {err}" + ) from err except ISYInvalidAuthError as err: _LOGGER.error( "Invalid credentials for the ISY, please adjust settings and try again: %s", @@ -187,16 +185,13 @@ async def async_setup_entry( ) return False except ISYConnectionError as err: - _LOGGER.error( - "Failed to connect to the ISY, please adjust settings and try again: %s", - err, - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Failed to connect to the ISY, please adjust settings and try again: {err}" + ) from err except ISYResponseParseError as err: - _LOGGER.warning( - "Error processing responses from the ISY; device may be busy, trying again later" - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Invalid XML response from ISY; Ensure the ISY is running the latest firmware: {err}" + ) from err _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) From fb21affe45d11c2e55d1c3e02d1d4d5c1062effd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 7 Jun 2021 12:50:08 +0200 Subject: [PATCH 209/750] Replace supported_features property with class attribute in deCONZ light entities (#51558) * Replace supported_features property with class attribute * attr_supported_features is already set to 0 --- homeassistant/components/deconz/light.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 838e7639fc7..60aa02c153e 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -106,24 +106,23 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) - self._features = 0 self.update_features(self._device) def update_features(self, device): """Calculate supported features of device.""" if device.brightness is not None: - self._features |= SUPPORT_BRIGHTNESS - self._features |= SUPPORT_FLASH - self._features |= SUPPORT_TRANSITION + self._attr_supported_features |= SUPPORT_BRIGHTNESS + self._attr_supported_features |= SUPPORT_FLASH + self._attr_supported_features |= SUPPORT_TRANSITION if device.ct is not None: - self._features |= SUPPORT_COLOR_TEMP + self._attr_supported_features |= SUPPORT_COLOR_TEMP if device.xy is not None or (device.hue is not None and device.sat is not None): - self._features |= SUPPORT_COLOR + self._attr_supported_features |= SUPPORT_COLOR if device.effect is not None: - self._features |= SUPPORT_EFFECT + self._attr_supported_features |= SUPPORT_EFFECT @property def brightness(self): @@ -158,11 +157,6 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Return true if light is on.""" return self._device.state - @property - def supported_features(self): - """Flag supported features.""" - return self._features - async def async_turn_on(self, **kwargs): """Turn on light.""" data = {"on": True} From 88386a7f449c812beedf67f7e2341cf7c58f4a28 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Jun 2021 13:24:07 +0200 Subject: [PATCH 210/750] Cleanup of Toon (#51230) --- .../components/toon/binary_sensor.py | 32 ++--- homeassistant/components/toon/climate.py | 11 +- homeassistant/components/toon/const.py | 118 +----------------- homeassistant/components/toon/models.py | 30 +---- homeassistant/components/toon/sensor.py | 33 +++-- homeassistant/components/toon/switch.py | 26 +--- 6 files changed, 44 insertions(+), 206 deletions(-) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 4a55911dcfc..de756225d57 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -61,27 +61,21 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity): def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None: """Initialize the Toon sensor.""" + super().__init__(coordinator) self.key = key - super().__init__( - coordinator, - enabled_default=BINARY_SENSOR_ENTITIES[key][ATTR_DEFAULT_ENABLED], - icon=BINARY_SENSOR_ENTITIES[key][ATTR_ICON], - name=BINARY_SENSOR_ENTITIES[key][ATTR_NAME], + sensor = BINARY_SENSOR_ENTITIES[key] + self._attr_name = sensor[ATTR_NAME] + self._attr_icon = sensor.get(ATTR_ICON) + self._attr_entity_registry_enabled_default = sensor.get( + ATTR_DEFAULT_ENABLED, True + ) + self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_unique_id = ( + # This unique ID is a bit ugly and contains unneeded information. + # It is here for legacy / backward compatible reasons. + f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_binary_sensor_{key}" ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this binary sensor.""" - agreement_id = self.coordinator.data.agreement.agreement_id - # This unique ID is a bit ugly and contains unneeded information. - # It is here for legacy / backward compatible reasons. - return f"{DOMAIN}_{agreement_id}_binary_sensor_{self.key}" - - @property - def device_class(self) -> str: - """Return the device class.""" - return BINARY_SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS] @property def is_on(self) -> bool | None: @@ -94,7 +88,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity): if value is None: return None - if BINARY_SENSOR_ENTITIES[self.key][ATTR_INVERTED]: + if BINARY_SENSOR_ENTITIES[self.key].get(ATTR_INVERTED, False): return not value return value diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index e69c178e595..a1201f85f7c 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -37,29 +37,26 @@ async def async_setup_entry( ) -> None: """Set up a Toon binary sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ToonThermostatDevice(coordinator, name="Thermostat", icon="mdi:thermostat")] - ) + async_add_entities([ToonThermostatDevice(coordinator)]) class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): """Representation of a Toon climate device.""" _attr_hvac_mode = HVAC_MODE_HEAT + _attr_icon = "mdi:thermostat" _attr_max_temp = DEFAULT_MAX_TEMP _attr_min_temp = DEFAULT_MIN_TEMP + _attr_name = "Thermostat" _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE _attr_temperature_unit = TEMP_CELSIUS def __init__( self, coordinator: ToonDataUpdateCoordinator, - *, - name: str, - icon: str, ) -> None: """Initialize Toon climate entity.""" - super().__init__(coordinator, name=name, icon=icon) + super().__init__(coordinator) self._attr_hvac_modes = [HVAC_MODE_HEAT] self._attr_preset_modes = [ PRESET_AWAY, diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 2946aacaa72..4d298f114c0 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -51,17 +51,13 @@ BINARY_SENSOR_ENTITIES = { ATTR_NAME: "Boiler Module Connection", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "boiler_module_connected", - ATTR_INVERTED: False, ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "thermostat_info_burner_info_1": { ATTR_NAME: "Boiler Heating", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "heating", - ATTR_INVERTED: False, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:fire", ATTR_DEFAULT_ENABLED: False, }, @@ -69,17 +65,12 @@ BINARY_SENSOR_ENTITIES = { ATTR_NAME: "Hot Tap Water", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "hot_tapwater", - ATTR_INVERTED: False, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water-pump", - ATTR_DEFAULT_ENABLED: True, }, "thermostat_info_burner_info_3": { ATTR_NAME: "Boiler Preheating", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "pre_heating", - ATTR_INVERTED: False, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:fire", ATTR_DEFAULT_ENABLED: False, }, @@ -87,25 +78,19 @@ BINARY_SENSOR_ENTITIES = { ATTR_NAME: "Boiler Burner", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "burner", - ATTR_INVERTED: False, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:fire", - ATTR_DEFAULT_ENABLED: True, }, "thermostat_info_error_found_255": { ATTR_NAME: "Boiler Status", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "error_found", - ATTR_INVERTED: False, ATTR_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, ATTR_ICON: "mdi:alert", - ATTR_DEFAULT_ENABLED: True, }, "thermostat_info_ot_communication_error_0": { ATTR_NAME: "OpenTherm Connection", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "opentherm_communication_error", - ATTR_INVERTED: False, ATTR_DEVICE_CLASS: DEVICE_CLASS_PROBLEM, ATTR_ICON: "mdi:check-network-outline", ATTR_DEFAULT_ENABLED: False, @@ -114,10 +99,7 @@ BINARY_SENSOR_ENTITIES = { ATTR_NAME: "Thermostat Program Override", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "program_overridden", - ATTR_INVERTED: False, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gesture-tap", - ATTR_DEFAULT_ENABLED: True, }, } @@ -128,76 +110,54 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current_display_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "gas_average": { ATTR_NAME: "Average Gas Usage", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "average", ATTR_UNIT_OF_MEASUREMENT: VOLUME_CM3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "gas_average_daily": { ATTR_NAME: "Average Daily Gas Usage", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "gas_daily_usage": { ATTR_NAME: "Gas Usage Today", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "gas_daily_cost": { ATTR_NAME: "Gas Cost Today", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "day_cost", ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "gas_meter_reading": { ATTR_NAME: "Gas Meter", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "meter", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", - ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_DEFAULT_ENABLED: False, }, "gas_value": { ATTR_NAME: "Current Gas Usage", ATTR_SECTION: "gas_usage", ATTR_MEASUREMENT: "current", ATTR_UNIT_OF_MEASUREMENT: VOLUME_CM3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:gas-cylinder", - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_average": { ATTR_NAME: "Average Power Usage", @@ -205,10 +165,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "average", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_average_daily": { ATTR_NAME: "Average Daily Energy Usage", @@ -216,21 +173,14 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_daily_cost": { ATTR_NAME: "Energy Cost Today", ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_cost", ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_daily_value": { ATTR_NAME: "Energy Usage Today", @@ -238,10 +188,6 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_meter_reading": { ATTR_NAME: "Electricity Meter Feed IN Tariff 1", @@ -249,10 +195,9 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_DEFAULT_ENABLED: False, }, "power_meter_reading_low": { ATTR_NAME: "Electricity Meter Feed IN Tariff 2", @@ -260,10 +205,9 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_DEFAULT_ENABLED: False, }, "power_value": { ATTR_NAME: "Current Power Usage", @@ -271,10 +215,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "solar_meter_reading_produced": { ATTR_NAME: "Electricity Meter Feed OUT Tariff 1", @@ -282,10 +223,9 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_DEFAULT_ENABLED: False, }, "solar_meter_reading_low_produced": { ATTR_NAME: "Electricity Meter Feed OUT Tariff 2", @@ -293,10 +233,9 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_DEFAULT_ENABLED: False, }, "solar_value": { ATTR_NAME: "Current Solar Power Production", @@ -304,10 +243,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current_solar", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "solar_maximum": { ATTR_NAME: "Max Solar Power Production Today", @@ -315,10 +251,6 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_max_solar", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "solar_produced": { ATTR_NAME: "Solar Power Production to Grid", @@ -326,10 +258,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current_produced", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, ATTR_STATE_CLASS: ATTR_MEASUREMENT, - ATTR_LAST_RESET: None, }, "power_usage_day_produced_solar": { ATTR_NAME: "Solar Energy Produced Today", @@ -337,10 +266,6 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_produced_solar", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, - ATTR_DEFAULT_ENABLED: True, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_usage_day_to_grid_usage": { ATTR_NAME: "Energy Produced To Grid Today", @@ -348,10 +273,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_to_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "power_usage_day_from_grid_usage": { ATTR_NAME: "Energy Usage From Grid Today", @@ -359,10 +281,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "day_from_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "solar_average_produced": { ATTR_NAME: "Average Solar Power Production to Grid", @@ -370,72 +289,54 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "average_produced", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "thermostat_info_current_modulation_level": { ATTR_NAME: "Boiler Modulation Level", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "current_modulation_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:percent", ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "power_usage_current_covered_by_solar": { ATTR_NAME: "Current Power Usage Covered By Solar", ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "current_covered_by_solar", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:solar-power", - ATTR_DEFAULT_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "water_average": { ATTR_NAME: "Average Water Usage", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "average", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LMIN, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "water_average_daily": { ATTR_NAME: "Average Daily Water Usage", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "water_daily_usage": { ATTR_NAME: "Water Usage Today", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, "water_meter_reading": { ATTR_NAME: "Water Meter", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "meter", ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, @@ -446,22 +347,17 @@ SENSOR_ENTITIES = { ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "current", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LMIN, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water-pump", ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: None, }, "water_daily_cost": { ATTR_NAME: "Water Cost Today", ATTR_SECTION: "water_usage", ATTR_MEASUREMENT: "day_cost", ATTR_UNIT_OF_MEASUREMENT: CURRENCY_EUR, - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:water-pump", ATTR_DEFAULT_ENABLED: False, - ATTR_STATE_CLASS: None, - ATTR_LAST_RESET: None, }, } @@ -470,16 +366,12 @@ SWITCH_ENTITIES = { ATTR_NAME: "Holiday Mode", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "holiday_mode", - ATTR_INVERTED: False, ATTR_ICON: "mdi:airport", - ATTR_DEFAULT_ENABLED: True, }, "thermostat_program": { ATTR_NAME: "Thermostat Program", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "program", - ATTR_INVERTED: False, ATTR_ICON: "mdi:calendar-clock", - ATTR_DEFAULT_ENABLED: True, }, } diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 18b44db45a8..7fb45af4d53 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -11,35 +11,7 @@ from .coordinator import ToonDataUpdateCoordinator class ToonEntity(CoordinatorEntity): """Defines a base Toon entity.""" - def __init__( - self, - coordinator: ToonDataUpdateCoordinator, - *, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the Toon entity.""" - super().__init__(coordinator) - self._enabled_default = enabled_default - self._icon = icon - self._name = name - self._state = None - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str | None: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default + coordinator: ToonDataUpdateCoordinator class ToonDisplayDeviceEntity(ToonEntity): diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 0e269c0bff3..90f74ceae87 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -120,26 +120,23 @@ class ToonSensor(ToonEntity, SensorEntity): def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None: """Initialize the Toon sensor.""" self.key = key + super().__init__(coordinator) - super().__init__( - coordinator, - enabled_default=SENSOR_ENTITIES[key][ATTR_DEFAULT_ENABLED], - icon=SENSOR_ENTITIES[key][ATTR_ICON], - name=SENSOR_ENTITIES[key][ATTR_NAME], + sensor = SENSOR_ENTITIES[key] + self._attr_entity_registry_enabled_default = sensor.get( + ATTR_DEFAULT_ENABLED, True + ) + self._attr_icon = sensor.get(ATTR_ICON) + self._attr_last_reset = sensor.get(ATTR_LAST_RESET) + self._attr_name = sensor[ATTR_NAME] + self._attr_state_class = sensor.get(ATTR_STATE_CLASS) + self._attr_unit_of_measurement = sensor[ATTR_UNIT_OF_MEASUREMENT] + self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_unique_id = ( + # This unique ID is a bit ugly and contains unneeded information. + # It is here for legacy / backward compatible reasons. + f"{DOMAIN}_{coordinator.data.agreement.agreement_id}_sensor_{key}" ) - - self._attr_last_reset = SENSOR_ENTITIES[key][ATTR_LAST_RESET] - self._attr_state_class = SENSOR_ENTITIES[key][ATTR_STATE_CLASS] - self._attr_unit_of_measurement = SENSOR_ENTITIES[key][ATTR_UNIT_OF_MEASUREMENT] - self._sttr_device_class = SENSOR_ENTITIES[key][ATTR_DEVICE_CLASS] - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - agreement_id = self.coordinator.data.agreement.agreement_id - # This unique ID is a bit ugly and contains unneeded information. - # It is here for legacy / backward compatible reasons. - return f"{DOMAIN}_{agreement_id}_sensor_{self.key}" @property def state(self) -> str | None: diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py index b830f53179e..06ca9c6631b 100644 --- a/homeassistant/components/toon/switch.py +++ b/homeassistant/components/toon/switch.py @@ -13,9 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import ( - ATTR_DEFAULT_ENABLED, ATTR_ICON, - ATTR_INVERTED, ATTR_MEASUREMENT, ATTR_NAME, ATTR_SECTION, @@ -44,19 +42,12 @@ class ToonSwitch(ToonEntity, SwitchEntity): def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None: """Initialize the Toon switch.""" self.key = key + super().__init__(coordinator) - super().__init__( - coordinator, - enabled_default=SWITCH_ENTITIES[key][ATTR_DEFAULT_ENABLED], - icon=SWITCH_ENTITIES[key][ATTR_ICON], - name=SWITCH_ENTITIES[key][ATTR_NAME], - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this binary sensor.""" - agreement_id = self.coordinator.data.agreement.agreement_id - return f"{agreement_id}_{self.key}" + switch = SWITCH_ENTITIES[key] + self._attr_icon = switch[ATTR_ICON] + self._attr_name = switch[ATTR_NAME] + self._attr_unique_id = f"{coordinator.data.agreement.agreement_id}_{key}" @property def is_on(self) -> bool: @@ -64,12 +55,7 @@ class ToonSwitch(ToonEntity, SwitchEntity): section = getattr( self.coordinator.data, SWITCH_ENTITIES[self.key][ATTR_SECTION] ) - value = getattr(section, SWITCH_ENTITIES[self.key][ATTR_MEASUREMENT]) - - if SWITCH_ENTITIES[self.key][ATTR_INVERTED]: - return not value - - return value + return getattr(section, SWITCH_ENTITIES[self.key][ATTR_MEASUREMENT]) class ToonProgramSwitch(ToonSwitch, ToonDisplayDeviceEntity): From f35929ba634099dcc98fa7eb61240508f82af532 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Jun 2021 14:50:31 +0200 Subject: [PATCH 211/750] Allow referencing sensor entities for before/after in time conditions (#51444) * Allow referencing sensor entities for before/after in time conditions * Fix typo in variable naming * Improve test coverage --- homeassistant/helpers/condition.py | 48 +++++++++--- homeassistant/helpers/config_validation.py | 8 +- tests/helpers/test_condition.py | 87 +++++++++++++++++++++- 3 files changed, 130 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index a467d952683..d3020b8d6d8 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -16,7 +16,9 @@ from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( async_get_device_automation_platform, ) +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, @@ -736,11 +738,24 @@ def time( after_entity = hass.states.get(after) if not after_entity: raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") - after = dt_util.dt.time( - after_entity.attributes.get("hour", 23), - after_entity.attributes.get("minute", 59), - after_entity.attributes.get("second", 59), - ) + if after_entity.domain == "input_datetime": + after = dt_util.dt.time( + after_entity.attributes.get("hour", 23), + after_entity.attributes.get("minute", 59), + after_entity.attributes.get("second", 59), + ) + elif after_entity.attributes.get( + ATTR_DEVICE_CLASS + ) == DEVICE_CLASS_TIMESTAMP and after_entity.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + after_datetime = dt_util.parse_datetime(after_entity.state) + if after_datetime is None: + return False + after = dt_util.as_local(after_datetime).time() + else: + return False if before is None: before = dt_util.dt.time(23, 59, 59, 999999) @@ -748,11 +763,24 @@ def time( before_entity = hass.states.get(before) if not before_entity: raise ConditionErrorMessage("time", f"unknown 'before' entity {before}") - before = dt_util.dt.time( - before_entity.attributes.get("hour", 23), - before_entity.attributes.get("minute", 59), - before_entity.attributes.get("second", 59), - ) + if before_entity.domain == "input_datetime": + before = dt_util.dt.time( + before_entity.attributes.get("hour", 23), + before_entity.attributes.get("minute", 59), + before_entity.attributes.get("second", 59), + ) + elif before_entity.attributes.get( + ATTR_DEVICE_CLASS + ) == DEVICE_CLASS_TIMESTAMP and before_entity.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + before_timedatime = dt_util.parse_datetime(before_entity.state) + if before_timedatime is None: + return False + before = dt_util.as_local(before_timedatime).time() + else: + return False if after < before: condition_trace_update_result(after=after, now_time=now_time, before=before) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 88f38f0a688..324173ed2e8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1014,8 +1014,12 @@ TIME_CONDITION_SCHEMA = vol.All( { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "time", - "before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), - "after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), + "before": vol.Any( + time, vol.All(str, entity_domain(["input_datetime", "sensor"])) + ), + "after": vol.Any( + time, vol.All(str, entity_domain(["input_datetime", "sensor"])) + ), "weekday": weekdays, } ), diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 973196d29f8..d75dd53bbf2 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -6,7 +6,8 @@ import pytest from homeassistant.components import sun import homeassistant.components.automation as automation -from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_DEVICE_CLASS, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import condition, trace from homeassistant.helpers.template import Template @@ -826,6 +827,90 @@ async def test_time_using_input_datetime(hass): condition.time(hass, before="input_datetime.not_existing") +async def test_time_using_sensor(hass): + """Test time conditions using sensor entities.""" + hass.states.async_set( + "sensor.am", + "2021-06-03 13:00:00.000000+00:00", # 6 am local time + {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + ) + hass.states.async_set( + "sensor.pm", + "2020-06-01 01:00:00.000000+00:00", # 6 pm local time + {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + ) + hass.states.async_set( + "sensor.no_device_class", + "2020-06-01 01:00:00.000000+00:00", + ) + hass.states.async_set( + "sensor.invalid_timestamp", + "This is not a timestamp", + {ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP}, + ) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=3), + ): + assert not condition.time(hass, after="sensor.am", before="sensor.pm") + assert condition.time(hass, after="sensor.pm", before="sensor.am") + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=9), + ): + assert condition.time(hass, after="sensor.am", before="sensor.pm") + assert not condition.time(hass, after="sensor.pm", before="sensor.am") + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=15), + ): + assert condition.time(hass, after="sensor.am", before="sensor.pm") + assert not condition.time(hass, after="sensor.pm", before="sensor.am") + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=21), + ): + assert not condition.time(hass, after="sensor.am", before="sensor.pm") + assert condition.time(hass, after="sensor.pm", before="sensor.am") + + # Trigger on PM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=18, minute=0, second=0), + ): + assert condition.time(hass, after="sensor.pm", before="sensor.am") + assert not condition.time(hass, after="sensor.am", before="sensor.pm") + assert condition.time(hass, after="sensor.pm") + assert not condition.time(hass, before="sensor.pm") + + # Even though valid, the device class is missing + assert not condition.time(hass, after="sensor.no_device_class") + assert not condition.time(hass, before="sensor.no_device_class") + + # Trigger on AM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=6, minute=0, second=0), + ): + assert not condition.time(hass, after="sensor.pm", before="sensor.am") + assert condition.time(hass, after="sensor.am", before="sensor.pm") + assert condition.time(hass, after="sensor.am") + assert not condition.time(hass, before="sensor.am") + + assert not condition.time(hass, after="sensor.invalid_timestamp") + assert not condition.time(hass, before="sensor.invalid_timestamp") + + with pytest.raises(ConditionError): + condition.time(hass, after="sensor.not_existing") + + with pytest.raises(ConditionError): + condition.time(hass, before="sensor.not_existing") + + async def test_state_raises(hass): """Test that state raises ConditionError on errors.""" # No entity From 4227a01e62cd3f0f81eab0e516dd0ce01d4e5d86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 14:54:18 +0200 Subject: [PATCH 212/750] Bump home-assistant/wheels from 2021.05.4 to 2021.06.0 (#51569) Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2021.05.4 to 2021.06.0. - [Release notes](https://github.com/home-assistant/wheels/releases) - [Commits](https://github.com/home-assistant/wheels/compare/2021.05.4...2021.06.0) --- updated-dependencies: - dependency-name: home-assistant/wheels dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ab506274585..02ae3ccbbd0 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -82,7 +82,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2021.05.4 + uses: home-assistant/wheels@2021.06.0 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} @@ -152,7 +152,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2021.05.4 + uses: home-assistant/wheels@2021.06.0 with: tag: ${{ matrix.tag }} arch: ${{ matrix.arch }} From 4c51299dcc7b690e4e6789cee826e2d67b50eed2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Jun 2021 15:02:15 +0200 Subject: [PATCH 213/750] Add easy converting string timestamps/dates to datetime objects in templates (#51576) --- homeassistant/helpers/template.py | 2 ++ tests/helpers/test_template.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f65100a8775..d991a0b58f2 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1420,6 +1420,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["atan"] = arc_tangent self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root + self.filters["as_datetime"] = dt_util.parse_datetime self.filters["as_timestamp"] = forgiving_as_timestamp self.filters["as_local"] = dt_util.as_local self.filters["timestamp_custom"] = timestamp_custom @@ -1454,6 +1455,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["atan"] = arc_tangent self.globals["atan2"] = arc_tangent2 self.globals["float"] = forgiving_float + self.globals["as_datetime"] = dt_util.parse_datetime self.globals["as_local"] = dt_util.as_local self.globals["as_timestamp"] = forgiving_as_timestamp self.globals["relative_time"] = relative_time diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 48ef6f25b67..1818d8c4876 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -526,6 +526,33 @@ def test_timestamp_local(hass): ) +@pytest.mark.parametrize( + "input", + ( + "2021-06-03 13:00:00.000000+00:00", + "1986-07-09T12:00:00Z", + "2016-10-19 15:22:05.588122+0100", + "2016-10-19", + "2021-01-01 00:00:01", + "invalid", + ), +) +def test_as_datetime(hass, input): + """Test converting a timestamp string to a date object.""" + expected = dt_util.parse_datetime(input) + if expected is not None: + expected = str(expected) + + assert ( + template.Template(f"{{{{ as_datetime('{input}') }}}}", hass).async_render() + == expected + ) + assert ( + template.Template(f"{{{{ '{input}' | as_datetime }}}}", hass).async_render() + == expected + ) + + def test_as_local(hass): """Test converting time to local.""" From 564042ec67913deda07ac382cac8f4d74a4f577a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 7 Jun 2021 15:45:58 +0200 Subject: [PATCH 214/750] Clean mysensors gateway type selection (#51531) * Clean mysensors gateway type selection * Fix comment grammar --- .../components/mysensors/config_flow.py | 2 +- homeassistant/components/mysensors/gateway.py | 69 ++++++++++--------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 223d27a2a60..56af6671958 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -347,7 +347,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): break # if no errors so far, try to connect - if not errors and not await try_connect(self.hass, user_input): + if not errors and not await try_connect(self.hass, gw_type, user_input): errors["base"] = "cannot_connect" return errors diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 424f80df729..ffbcc47fc03 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -22,6 +22,9 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_BAUD_RATE, CONF_DEVICE, + CONF_GATEWAY_TYPE, + CONF_GATEWAY_TYPE_MQTT, + CONF_GATEWAY_TYPE_SERIAL, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, @@ -31,6 +34,7 @@ from .const import ( DOMAIN, MYSENSORS_GATEWAY_START_TASK, MYSENSORS_GATEWAYS, + ConfGatewayType, GatewayId, ) from .handler import HANDLERS @@ -66,10 +70,12 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -async def try_connect(hass: HomeAssistant, user_input: dict[str, Any]) -> bool: +async def try_connect( + hass: HomeAssistant, gateway_type: ConfGatewayType, user_input: dict[str, Any] +) -> bool: """Try to connect to a gateway and report if it worked.""" - if user_input[CONF_DEVICE] == MQTT_COMPONENT: - return True # dont validate mqtt. mqtt gateways dont send ready messages :( + if gateway_type == "MQTT": + return True # Do not validate MQTT, as that does not use connection made. try: gateway_ready = asyncio.Event() @@ -78,6 +84,7 @@ async def try_connect(hass: HomeAssistant, user_input: dict[str, Any]) -> bool: gateway: BaseAsyncGateway | None = await _get_gateway( hass, + gateway_type, device=user_input[CONF_DEVICE], version=user_input[CONF_VERSION], event_callback=lambda _: None, @@ -128,6 +135,7 @@ async def setup_gateway( ready_gateway = await _get_gateway( hass, + gateway_type=entry.data[CONF_GATEWAY_TYPE], device=entry.data[CONF_DEVICE], version=entry.data[CONF_VERSION], event_callback=_gw_callback_factory(hass, entry.entry_id), @@ -145,6 +153,7 @@ async def setup_gateway( async def _get_gateway( hass: HomeAssistant, + gateway_type: ConfGatewayType, device: str, version: str, event_callback: Callable[[Message], None], @@ -154,15 +163,16 @@ async def _get_gateway( topic_in_prefix: str | None = None, topic_out_prefix: str | None = None, retain: bool = False, - persistence: bool = True, # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence + persistence: bool = True, ) -> BaseAsyncGateway | None: """Return gateway after setup of the gateway.""" if persistence_file is not None: - # interpret relative paths to be in hass config folder. absolute paths will be left as they are + # Interpret relative paths to be in hass config folder. + # Absolute paths will be left as they are. persistence_file = hass.config.path(persistence_file) - if device == MQTT_COMPONENT: + if gateway_type == CONF_GATEWAY_TYPE_MQTT: # Make sure the mqtt integration is set up. # Naive check that doesn't consider config entry state. if MQTT_DOMAIN not in hass.config.components: @@ -195,35 +205,26 @@ async def _get_gateway( persistence_file=persistence_file, protocol_version=version, ) + elif gateway_type == CONF_GATEWAY_TYPE_SERIAL: + gateway = mysensors.AsyncSerialGateway( + device, + baud=baud_rate, + loop=hass.loop, + event_callback=None, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version, + ) else: - try: - await hass.async_add_executor_job(is_serial_port, device) - gateway = mysensors.AsyncSerialGateway( - device, - baud=baud_rate, - loop=hass.loop, - event_callback=None, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, - ) - except vol.Invalid: - try: - await hass.async_add_executor_job(is_socket_address, device) - # valid ip address - gateway = mysensors.AsyncTCPGateway( - device, - port=tcp_port, - loop=hass.loop, - event_callback=None, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, - ) - except vol.Invalid: - # invalid ip address - _LOGGER.error("Connect failed: Invalid device %s", device) - return None + gateway = mysensors.AsyncTCPGateway( + device, + port=tcp_port, + loop=hass.loop, + event_callback=None, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version, + ) gateway.event_callback = event_callback if persistence: await gateway.start_persistence() From 7560a77e0e5c858d46c3cf33422a271cf9fa3234 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 7 Jun 2021 16:04:04 +0200 Subject: [PATCH 215/750] Type mysensors strictly (#51535) --- .strict-typing | 1 + .../components/mysensors/__init__.py | 11 +-- .../components/mysensors/binary_sensor.py | 11 ++- homeassistant/components/mysensors/climate.py | 55 +++++++------ .../components/mysensors/config_flow.py | 8 +- homeassistant/components/mysensors/const.py | 55 +++++++------ homeassistant/components/mysensors/cover.py | 29 +++---- homeassistant/components/mysensors/device.py | 71 +++++++++++------ .../components/mysensors/device_tracker.py | 23 ++++-- homeassistant/components/mysensors/gateway.py | 36 ++++++--- homeassistant/components/mysensors/handler.py | 6 +- homeassistant/components/mysensors/helpers.py | 7 +- homeassistant/components/mysensors/light.py | 77 ++++++++++--------- homeassistant/components/mysensors/notify.py | 27 +++++-- homeassistant/components/mysensors/sensor.py | 41 ++++++---- homeassistant/components/mysensors/switch.py | 52 +++++++++---- mypy.ini | 11 +++ 17 files changed, 329 insertions(+), 192 deletions(-) diff --git a/.strict-typing b/.strict-typing index e11374312da..23b4ed76513 100644 --- a/.strict-typing +++ b/.strict-typing @@ -46,6 +46,7 @@ homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.network.* homeassistant.components.notify.* diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 068fd9361fa..2a958cee060 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -40,6 +40,7 @@ from .const import ( MYSENSORS_ON_UNLOAD, PLATFORMS_WITH_ENTRY_SUPPORT, DevId, + DiscoveryInfo, SensorType, ) from .device import MySensorsDevice, get_mysensors_devices @@ -70,7 +71,7 @@ def set_default_persistence_file(value: dict) -> dict: return value -def has_all_unique_files(value): +def has_all_unique_files(value: list[dict]) -> list[dict]: """Validate that all persistence files are unique and set if any is set.""" persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] schema = vol.Schema(vol.Unique()) @@ -78,17 +79,17 @@ def has_all_unique_files(value): return value -def is_persistence_file(value): +def is_persistence_file(value: str) -> str: """Validate that persistence file path ends in either .pickle or .json.""" if value.endswith((".json", ".pickle")): return value raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") -def deprecated(key): +def deprecated(key: str) -> Callable[[dict], dict]: """Mark key as deprecated in configuration.""" - def validator(config): + def validator(config: dict) -> dict: """Check if key is in config, log warning and remove key.""" if key not in config: return config @@ -270,7 +271,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def setup_mysensors_platform( hass: HomeAssistant, domain: str, # hass platform name - discovery_info: dict[str, list[DevId]], + discovery_info: DiscoveryInfo, device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsDevice]], device_args: ( None | tuple diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 8358c3b2ecf..f94b5f71728 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,4 +1,6 @@ """Support for MySensors binary sensors.""" +from __future__ import annotations + from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, @@ -17,6 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import DiscoveryInfo from .helpers import on_unload SENSORS = { @@ -35,11 +38,11 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" @callback - def async_discover(discovery_info): + def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors binary_sensor.""" mysensors.setup_mysensors_platform( hass, @@ -64,12 +67,12 @@ class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity """Representation of a MySensors Binary Sensor child node.""" @property - def is_on(self): + def is_on(self) -> bool: """Return True if the binary sensor is on.""" return self._values.get(self.value_type) == STATE_ON @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this sensor, from DEVICE_CLASSES.""" pres = self.gateway.const.Presentation device_class = SENSORS.get(pres(self.child_type).name) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 797fcfafcc7..5dd52673581 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,4 +1,8 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components import mysensors from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -13,7 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant @@ -43,10 +47,10 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" - async def async_discover(discovery_info): + async def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors climate.""" mysensors.setup_mysensors_platform( hass, @@ -71,7 +75,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): """Representation of a MySensors HVAC.""" @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" features = 0 set_req = self.gateway.const.SetReq @@ -87,22 +91,23 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): return features @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" - value = self._values.get(self.gateway.const.SetReq.V_TEMP) + value: str | None = self._values.get(self.gateway.const.SetReq.V_TEMP) + float_value: float | None = None if value is not None: - value = float(value) + float_value = float(value) - return value + return float_value @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" set_req = self.gateway.const.SetReq if ( @@ -116,42 +121,46 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): return float(temp) if temp is not None else None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_HEAT in self._values: temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) return float(temp) if temp is not None else None + return None + @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" set_req = self.gateway.const.SetReq if set_req.V_HVAC_SETPOINT_COOL in self._values: temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) return float(temp) if temp is not None else None - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - return self._values.get(self.value_type) + return None @property - def hvac_modes(self): + def hvac_mode(self) -> str: + """Return current operation ie. heat, cool, idle.""" + return self._values.get(self.value_type, HVAC_MODE_HEAT) + + @property + def hvac_modes(self) -> list[str]: """List of available operation modes.""" return OPERATION_LIST @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED) @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """List of available fan modes.""" return FAN_LIST - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" set_req = self.gateway.const.SetReq temp = kwargs.get(ATTR_TEMPERATURE) @@ -183,7 +192,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): self._values[value_type] = value self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -194,7 +203,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): self._values[set_req.V_HVAC_SPEED] = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target temperature.""" self.gateway.set_child_value( self.node_id, @@ -208,7 +217,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): self._values[self.value_type] = hvac_mode self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Update the controller with the latest value from a sensor.""" await super().async_update() self._values[self.value_type] = DICT_MYS_TO_HA[self._values[self.value_type]] diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 56af6671958..1abf45dd60f 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -82,8 +82,8 @@ def _validate_version(version: str) -> dict[str, str]: def _is_same_device( - gw_type: ConfGatewayType, user_input: dict[str, str], entry: ConfigEntry -): + gw_type: ConfGatewayType, user_input: dict[str, Any], entry: ConfigEntry +) -> bool: """Check if another ConfigDevice is actually the same as user_input. This function only compares addresses and tcp ports, so it is possible to fool it with tricks like port forwarding. @@ -91,7 +91,9 @@ def _is_same_device( if entry.data[CONF_DEVICE] != user_input[CONF_DEVICE]: return False if gw_type == CONF_GATEWAY_TYPE_TCP: - return entry.data[CONF_TCP_PORT] == user_input[CONF_TCP_PORT] + entry_tcp_port: int = entry.data[CONF_TCP_PORT] + input_tcp_port: int = user_input[CONF_TCP_PORT] + return entry_tcp_port == input_tcp_port if gw_type == CONF_GATEWAY_TYPE_MQTT: entry_topics = { entry.data[CONF_TOPIC_IN_PREFIX], diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 1bd071be9a9..f8e157e3622 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -2,23 +2,23 @@ from __future__ import annotations from collections import defaultdict -from typing import Literal, Tuple +from typing import Final, Literal, Tuple, TypedDict -ATTR_DEVICES: str = "devices" -ATTR_GATEWAY_ID: str = "gateway_id" +ATTR_DEVICES: Final = "devices" +ATTR_GATEWAY_ID: Final = "gateway_id" -CONF_BAUD_RATE: str = "baud_rate" -CONF_DEVICE: str = "device" -CONF_GATEWAYS: str = "gateways" -CONF_NODES: str = "nodes" -CONF_PERSISTENCE: str = "persistence" -CONF_PERSISTENCE_FILE: str = "persistence_file" -CONF_RETAIN: str = "retain" -CONF_TCP_PORT: str = "tcp_port" -CONF_TOPIC_IN_PREFIX: str = "topic_in_prefix" -CONF_TOPIC_OUT_PREFIX: str = "topic_out_prefix" -CONF_VERSION: str = "version" -CONF_GATEWAY_TYPE: str = "gateway_type" +CONF_BAUD_RATE: Final = "baud_rate" +CONF_DEVICE: Final = "device" +CONF_GATEWAYS: Final = "gateways" +CONF_NODES: Final = "nodes" +CONF_PERSISTENCE: Final = "persistence" +CONF_PERSISTENCE_FILE: Final = "persistence_file" +CONF_RETAIN: Final = "retain" +CONF_TCP_PORT: Final = "tcp_port" +CONF_TOPIC_IN_PREFIX: Final = "topic_in_prefix" +CONF_TOPIC_OUT_PREFIX: Final = "topic_out_prefix" +CONF_VERSION: Final = "version" +CONF_GATEWAY_TYPE: Final = "gateway_type" ConfGatewayType = Literal["Serial", "TCP", "MQTT"] CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial" CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP" @@ -29,19 +29,28 @@ CONF_GATEWAY_TYPE_ALL: list[str] = [ CONF_GATEWAY_TYPE_TCP, ] -DOMAIN: str = "mysensors" +DOMAIN: Final = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" -MYSENSORS_GATEWAYS: str = "mysensors_gateways" -PLATFORM: str = "platform" -SCHEMA: str = "schema" +MYSENSORS_GATEWAYS: Final = "mysensors_gateways" +PLATFORM: Final = "platform" +SCHEMA: Final = "schema" CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}" NODE_CALLBACK: str = "mysensors_node_callback_{}_{}" -MYSENSORS_DISCOVERY = "mysensors_discovery_{}_{}" -MYSENSORS_ON_UNLOAD = "mysensors_on_unload_{}" -TYPE: str = "type" +MYSENSORS_DISCOVERY: str = "mysensors_discovery_{}_{}" +MYSENSORS_ON_UNLOAD: str = "mysensors_on_unload_{}" +TYPE: Final = "type" UPDATE_DELAY: float = 0.1 -SERVICE_SEND_IR_CODE: str = "send_ir_code" + +class DiscoveryInfo(TypedDict): + """Represent the discovery info type for mysensors platforms.""" + + devices: list[DevId] + name: str # CONF_NAME is used in the notify base integration. + gateway_id: GatewayId + + +SERVICE_SEND_IR_CODE: Final = "send_ir_code" SensorType = str # S_DOOR, S_MOTION, S_SMOKE, ... diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 2d852ef05b4..bab7a07a867 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,10 +1,13 @@ """Support for MySensors covers.""" +from __future__ import annotations + from enum import Enum, unique import logging +from typing import Any from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY +from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY, DiscoveryInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -30,10 +33,10 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" - async def async_discover(discovery_info): + async def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors cover.""" mysensors.setup_mysensors_platform( hass, @@ -57,7 +60,7 @@ async def async_setup_entry( class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): """Representation of the value of a MySensors Cover child node.""" - def get_cover_state(self): + def get_cover_state(self) -> CoverState: """Return a CoverState enum representing the state of the cover.""" set_req = self.gateway.const.SetReq v_up = self._values.get(set_req.V_UP) == STATE_ON @@ -69,7 +72,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): # or V_STATUS. amount = 100 if set_req.V_DIMMER in self._values: - amount = self._values.get(set_req.V_DIMMER) + amount = self._values[set_req.V_DIMMER] else: amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0 @@ -82,22 +85,22 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): return CoverState.OPEN @property - def is_closed(self): + def is_closed(self) -> bool: """Return True if the cover is closed.""" return self.get_cover_state() == CoverState.CLOSED @property - def is_closing(self): + def is_closing(self) -> bool: """Return True if the cover is closing.""" return self.get_cover_state() == CoverState.CLOSING @property - def is_opening(self): + def is_opening(self) -> bool: """Return True if the cover is opening.""" return self.get_cover_state() == CoverState.OPENING @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. @@ -105,7 +108,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_DIMMER) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -119,7 +122,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self._values[set_req.V_LIGHT] = STATE_ON self.async_write_ha_state() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -133,7 +136,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self._values[set_req.V_LIGHT] = STATE_OFF self.async_write_ha_state() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq @@ -145,7 +148,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): self._values[set_req.V_DIMMER] = position self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index c066e633eaa..32305061ca7 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -66,10 +66,10 @@ class MySensorsDevice: return self.gateway_id, self.node_id, self.child_id, self.value_type @property - def _logger(self): + def _logger(self) -> logging.Logger: return logging.getLogger(f"{__name__}.{self.name}") - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove this entity from home assistant.""" for platform in PLATFORM_TYPES: platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform) @@ -91,17 +91,26 @@ class MySensorsDevice: @property def sketch_name(self) -> str: - """Return the name of the sketch running on the whole node (will be the same for several entities!).""" - return self._node.sketch_name + """Return the name of the sketch running on the whole node. + + The name will be the same for several entities. + """ + return self._node.sketch_name # type: ignore[no-any-return] @property def sketch_version(self) -> str: - """Return the version of the sketch running on the whole node (will be the same for several entities!).""" - return self._node.sketch_version + """Return the version of the sketch running on the whole node. + + The name will be the same for several entities. + """ + return self._node.sketch_version # type: ignore[no-any-return] @property def node_name(self) -> str: - """Name of the whole node (will be the same for several entities!).""" + """Name of the whole node. + + The name will be the same for several entities. + """ return f"{self.sketch_name} {self.node_id}" @property @@ -111,7 +120,7 @@ class MySensorsDevice: @property def device_info(self) -> DeviceInfo: - """Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" + """Return the device info.""" return { "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, "name": self.node_name, @@ -120,13 +129,13 @@ class MySensorsDevice: } @property - def name(self): + def name(self) -> str: """Return the name of this entity.""" return f"{self.node_name} {self.child_id}" @property - def extra_state_attributes(self): - """Return device specific state attributes.""" + def _extra_attributes(self) -> dict[str, Any]: + """Return device specific attributes.""" node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] attr = { @@ -136,10 +145,6 @@ class MySensorsDevice: ATTR_DESCRIPTION: child.description, ATTR_NODE_ID: self.node_id, } - # This works when we are actually an Entity (i.e. all platforms except device_tracker) - if hasattr(self, "platform"): - # pylint: disable=no-member - attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE] set_req = self.gateway.const.SetReq @@ -148,7 +153,7 @@ class MySensorsDevice: return attr - async def async_update(self): + async def async_update(self) -> None: """Update the controller with the latest value from a sensor.""" node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] @@ -175,17 +180,17 @@ class MySensorsDevice: else: self._values[value_type] = value - async def _async_update_callback(self): + async def _async_update_callback(self) -> None: """Update the device.""" raise NotImplementedError @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the device after delay.""" if self._update_scheduled: return - async def update(): + async def update() -> None: """Perform update.""" try: await self._async_update_callback() @@ -199,31 +204,47 @@ class MySensorsDevice: self.hass.loop.call_later(UPDATE_DELAY, delayed_update) -def get_mysensors_devices(hass, domain: str) -> dict[DevId, MySensorsDevice]: +def get_mysensors_devices( + hass: HomeAssistant, domain: str +) -> dict[DevId, MySensorsDevice]: """Return MySensors devices for a hass platform name.""" if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]: hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} - return hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] + devices: dict[DevId, MySensorsDevice] = hass.data[DOMAIN][ + MYSENSORS_PLATFORM_DEVICES.format(domain) + ] + return devices class MySensorsEntity(MySensorsDevice, Entity): """Representation of a MySensors entity.""" @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state. The gateway pushes its states.""" return False @property - def available(self): + def available(self) -> bool: """Return true if entity is available.""" return self.value_type in self._values - async def _async_update_callback(self): + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return entity specific state attributes.""" + attr = self._extra_attributes + + assert self.platform + assert self.platform.config_entry + attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE] + + return attr + + async def _async_update_callback(self) -> None: """Update the entity.""" await self.async_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update callback.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 6297f8344fc..544fb8d6b09 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,8 +1,16 @@ """Support for tracking MySensors devices.""" +from __future__ import annotations + +from typing import Any, Callable + from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.mysensors import DevId -from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId +from homeassistant.components.mysensors.const import ( + ATTR_GATEWAY_ID, + DiscoveryInfo, + GatewayId, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -11,8 +19,11 @@ from .helpers import on_unload async def async_setup_scanner( - hass: HomeAssistant, config, async_see, discovery_info=None -): + hass: HomeAssistant, + config: dict[str, Any], + async_see: Callable, + discovery_info: DiscoveryInfo | None = None, +) -> bool: """Set up the MySensors device scanner.""" if not discovery_info: return False @@ -55,13 +66,13 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistant, async_see, *args): + def __init__(self, hass: HomeAssistant, async_see: Callable, *args: Any) -> None: """Set up instance.""" super().__init__(*args) self.async_see = async_see self.hass = hass - async def _async_update_callback(self): + async def _async_update_callback(self) -> None: """Update the device.""" await self.async_update() node = self.gateway.sensors[self.node_id] @@ -74,5 +85,5 @@ class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): host_name=self.name, gps=(latitude, longitude), battery=node.battery_level, - attributes=self.extra_state_attributes, + attributes=self._extra_attributes, ) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index ffbcc47fc03..f1e2cd0a4e1 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -14,6 +14,10 @@ from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt.models import ( + Message as MQTTMessage, + PublishPayloadType, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -51,7 +55,7 @@ GATEWAY_READY_TIMEOUT = 20.0 MQTT_COMPONENT = "mqtt" -def is_serial_port(value): +def is_serial_port(value: str) -> str: """Validate that value is a windows serial port or a unix device.""" if sys.platform.startswith("win"): ports = (f"COM{idx + 1}" for idx in range(256)) @@ -61,7 +65,7 @@ def is_serial_port(value): return cv.isdevice(value) -def is_socket_address(value): +def is_socket_address(value: str) -> str: """Validate that value is a valid address.""" try: socket.getaddrinfo(value, None) @@ -179,15 +183,17 @@ async def _get_gateway( return None mqtt = hass.components.mqtt - def pub_callback(topic, payload, qos, retain): + def pub_callback(topic: str, payload: str, qos: int, retain: bool) -> None: """Call MQTT publish function.""" mqtt.async_publish(topic, payload, qos, retain) - def sub_callback(topic, sub_cb, qos): + def sub_callback( + topic: str, sub_cb: Callable[[str, PublishPayloadType, int], None], qos: int + ) -> None: """Call MQTT subscribe function.""" @callback - def internal_callback(msg): + def internal_callback(msg: MQTTMessage) -> None: """Call callback.""" sub_cb(msg.topic, msg.payload, msg.qos) @@ -234,7 +240,7 @@ async def _get_gateway( async def finish_setup( hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway -): +) -> None: """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] start_tasks = [] @@ -249,7 +255,7 @@ async def finish_setup( async def _discover_persistent_devices( hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway -): +) -> None: """Discover platforms for devices loaded via persistence file.""" new_devices = defaultdict(list) for node_id in gateway.sensors: @@ -265,7 +271,9 @@ async def _discover_persistent_devices( discover_mysensors_platform(hass, entry.entry_id, platform, dev_ids) -async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): +async def gw_stop( + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway +) -> None: """Stop the gateway.""" connect_task = hass.data[DOMAIN].pop( MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id), None @@ -275,11 +283,14 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): await gateway.stop() -async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway): +async def _gw_start( + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway +) -> None: """Start the gateway.""" gateway_ready = asyncio.Event() - def gateway_connected(_: BaseAsyncGateway): + def gateway_connected(_: BaseAsyncGateway) -> None: + """Handle gateway connected.""" gateway_ready.set() gateway.on_conn_made = gateway_connected @@ -290,7 +301,8 @@ async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncG gateway.start() ) # store the connect task so it can be cancelled in gw_stop - async def stop_this_gw(_: Event): + async def stop_this_gw(_: Event) -> None: + """Stop the gateway.""" await gw_stop(hass, entry, gateway) on_unload( @@ -319,7 +331,7 @@ def _gw_callback_factory( """Return a new callback for the gateway.""" @callback - def mysensors_callback(msg: Message): + def mysensors_callback(msg: Message) -> None: """Handle messages from a MySensors gateway. All MySenors messages are received here. diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 8558cd01f42..0fb86fd0eec 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -68,7 +68,7 @@ async def handle_sketch_version( @callback def _handle_child_update( hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]] -): +) -> None: """Handle a child update.""" signals: list[str] = [] @@ -91,7 +91,9 @@ def _handle_child_update( @callback -def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message): +def _handle_node_update( + hass: HomeAssistant, gateway_id: GatewayId, msg: Message +) -> None: """Handle a node update.""" signal = NODE_CALLBACK.format(gateway_id, msg.node_id) async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 5bc0a0d0839..7c50526cd6e 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -117,7 +117,10 @@ def switch_ir_send_schema( def get_child_schema( - gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType, schema + gateway: BaseAsyncGateway, + child: ChildSensor, + value_type_name: ValueType, + schema: dict, ) -> vol.Schema: """Return a child schema.""" set_req = gateway.const.SetReq @@ -136,7 +139,7 @@ def get_child_schema( def invalid_msg( gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType -): +) -> str: """Return a message for an invalid child during schema validation.""" pres = gateway.const.Presentation set_req = gateway.const.SetReq diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index b9fbc139e4f..81089f052b6 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -1,4 +1,8 @@ """Support for MySensors lights.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -10,7 +14,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback @@ -19,6 +22,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType +from .device import MySensorsDevice from .helpers import on_unload SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE @@ -28,15 +33,15 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" - device_class_map = { + device_class_map: dict[SensorType, type[MySensorsDevice]] = { "S_DIMMER": MySensorsLightDimmer, "S_RGB_LIGHT": MySensorsLightRGB, "S_RGBW_LIGHT": MySensorsLightRGBW, } - async def async_discover(discovery_info): + async def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors light.""" mysensors.setup_mysensors_platform( hass, @@ -60,35 +65,35 @@ async def async_setup_entry( class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): """Representation of a MySensors Light child node.""" - def __init__(self, *args): + def __init__(self, *args: Any) -> None: """Initialize a MySensors Light.""" super().__init__(*args) - self._state = None - self._brightness = None - self._hs = None - self._white = None + self._state: bool | None = None + self._brightness: int | None = None + self._hs: tuple[int, int] | None = None + self._white: int | None = None @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return self._brightness @property - def hs_color(self): + def hs_color(self) -> tuple[int, int] | None: """Return the hs color value [int, int].""" return self._hs @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value of this light between 0..255.""" return self._white @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self._state + return bool(self._state) - def _turn_on_light(self): + def _turn_on_light(self) -> None: """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -103,10 +108,9 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self._state = True self._values[set_req.V_LIGHT] = STATE_ON - def _turn_on_dimmer(self, **kwargs): + def _turn_on_dimmer(self, **kwargs: Any) -> None: """Turn on dimmer child device.""" set_req = self.gateway.const.SetReq - brightness = self._brightness if ( ATTR_BRIGHTNESS not in kwargs @@ -114,7 +118,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): or set_req.V_DIMMER not in self._values ): return - brightness = kwargs[ATTR_BRIGHTNESS] + brightness: int = kwargs[ATTR_BRIGHTNESS] percent = round(100 * brightness / 255) self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 @@ -125,17 +129,20 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self._brightness = brightness self._values[set_req.V_DIMMER] = percent - def _turn_on_rgb_and_w(self, hex_template, **kwargs): + def _turn_on_rgb_and_w(self, hex_template: str, **kwargs: Any) -> None: """Turn on RGB or RGBW child device.""" + assert self._hs + assert self._white is not None rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) - hs_color = kwargs.get(ATTR_HS_COLOR) + hs_color: tuple[float, float] | None = kwargs.get(ATTR_HS_COLOR) + new_rgb: tuple[int, int, int] | None if hs_color is not None: new_rgb = color_util.color_hs_to_RGB(*hs_color) else: new_rgb = None - new_white = kwargs.get(ATTR_WHITE_VALUE) + new_white: int | None = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: return @@ -155,11 +162,11 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): if self.assumed_state: # optimistically assume that light has changed state - self._hs = color_util.color_RGB_to_hs(*rgb) + self._hs = color_util.color_RGB_to_hs(*rgb) # type: ignore[assignment] self._white = white self._values[self.value_type] = hex_color - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) @@ -170,13 +177,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self.async_write_ha_state() @callback - def _async_update_light(self): + def _async_update_light(self) -> None: """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT self._state = self._values[value_type] == STATE_ON @callback - def _async_update_dimmer(self): + def _async_update_dimmer(self) -> None: """Update the controller with values from dimmer child.""" value_type = self.gateway.const.SetReq.V_DIMMER if value_type in self._values: @@ -185,31 +192,31 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): self._state = False @callback - def _async_update_rgb_or_w(self): + def _async_update_rgb_or_w(self) -> None: """Update the controller with values from RGB or RGBW child.""" value = self._values[self.value_type] color_list = rgb_hex_to_rgb_list(value) if len(color_list) > 3: self._white = color_list.pop() - self._hs = color_util.color_RGB_to_hs(*color_list) + self._hs = color_util.color_RGB_to_hs(*color_list) # type: ignore[assignment] class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) if self.assumed_state: self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Update the controller with the latest value from a sensor.""" await super().async_update() self._async_update_light() @@ -220,14 +227,14 @@ class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" set_req = self.gateway.const.SetReq if set_req.V_DIMMER in self._values: return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_COLOR - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) @@ -235,7 +242,7 @@ class MySensorsLightRGB(MySensorsLight): if self.assumed_state: self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Update the controller with the latest value from a sensor.""" await super().async_update() self._async_update_light() @@ -247,14 +254,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): """RGBW child class to MySensorsLightRGB.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" set_req = self.gateway.const.SetReq if set_req.V_DIMMER in self._values: return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index 50fca55ab39..109b357dee7 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -1,9 +1,20 @@ """MySensors notification service.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components import mysensors from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService +from homeassistant.core import HomeAssistant + +from .const import DevId, DiscoveryInfo -async def async_get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: dict[str, Any], + discovery_info: DiscoveryInfo | None = None, +) -> BaseNotificationService | None: """Get the MySensors notification service.""" if not discovery_info: return None @@ -19,7 +30,7 @@ async def async_get_service(hass, config, discovery_info=None): class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): """Represent a MySensors Notification device.""" - def send_msg(self, msg): + def send_msg(self, msg: str) -> None: """Send a message.""" for sub_msg in [msg[i : i + 25] for i in range(0, len(msg), 25)]: # Max mysensors payload is 25 bytes. @@ -27,7 +38,7 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): self.node_id, self.child_id, self.value_type, sub_msg ) - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" return f"" @@ -35,11 +46,15 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): class MySensorsNotificationService(BaseNotificationService): """Implement a MySensors notification service.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the service.""" - self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) + self.devices: dict[ + DevId, MySensorsNotificationDevice + ] = mysensors.get_mysensors_devices( + hass, DOMAIN + ) # type: ignore[assignment] - async def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) devices = [ diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index abbfa66ab8e..2ede5e38c6a 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,8 +1,9 @@ """Support for MySensors sensors.""" +from __future__ import annotations + from awesomeversion import AwesomeVersion from homeassistant.components import mysensors -from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,9 +27,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload -SENSORS = { +SENSORS: dict[str, list[str | None] | dict[str, list[str | None]]] = { "V_TEMP": [None, "mdi:thermometer"], "V_HUM": [PERCENTAGE, "mdi:water-percent"], "V_DIMMER": [PERCENTAGE, "mdi:percent"], @@ -67,10 +69,10 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" - async def async_discover(discovery_info): + async def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors sensor.""" mysensors.setup_mysensors_platform( hass, @@ -95,7 +97,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Representation of a MySensors Sensor child node.""" @property - def force_update(self): + def force_update(self) -> bool: """Return True if state updates should be forced. If True, a state change will be triggered anytime the state property is @@ -104,36 +106,43 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): return True @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self._values.get(self.value_type) @property - def icon(self): + def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" icon = self._get_sensor_type()[1] return icon @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" set_req = self.gateway.const.SetReq if ( AwesomeVersion(self.gateway.protocol_version) >= AwesomeVersion("1.5") and set_req.V_UNIT_PREFIX in self._values ): - return self._values[set_req.V_UNIT_PREFIX] + custom_unit: str = self._values[set_req.V_UNIT_PREFIX] + return custom_unit + + if set_req(self.value_type) == set_req.V_TEMP: + if self.hass.config.units.is_metric: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + unit = self._get_sensor_type()[0] return unit - def _get_sensor_type(self): + def _get_sensor_type(self) -> list[str | None]: """Return list with unit and icon of sensor type.""" pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - SENSORS[set_req.V_TEMP.name][0] = ( - TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT - ) - sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) - if isinstance(sensor_type, dict): - sensor_type = sensor_type.get(pres(self.child_type).name, [None, None]) + + _sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + if isinstance(_sensor_type, dict): + sensor_type = _sensor_type.get(pres(self.child_type).name, [None, None]) + else: + sensor_type = _sensor_type return sensor_type diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 3910df55eec..cdb4979d16b 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,16 +1,28 @@ """Support for MySensors switches.""" +from __future__ import annotations + +from contextlib import suppress +from typing import Any + import voluptuous as vol from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from ...config_entries import ConfigEntry from ...helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE +from .const import ( + DOMAIN as MYSENSORS_DOMAIN, + MYSENSORS_DISCOVERY, + SERVICE_SEND_IR_CODE, + DiscoveryInfo, + SensorType, +) +from .device import MySensorsDevice from .helpers import on_unload ATTR_IR_CODE = "V_IR_SEND" @@ -24,9 +36,9 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up this platform for a specific ConfigEntry(==Gateway).""" - device_class_map = { + device_class_map: dict[SensorType, type[MySensorsDevice]] = { "S_DOOR": MySensorsSwitch, "S_MOTION": MySensorsSwitch, "S_SMOKE": MySensorsSwitch, @@ -42,7 +54,7 @@ async def async_setup_entry( "S_WATER_QUALITY": MySensorsSwitch, } - async def async_discover(discovery_info): + async def async_discover(discovery_info: DiscoveryInfo) -> None: """Discover and add a MySensors switch.""" mysensors.setup_mysensors_platform( hass, @@ -52,7 +64,7 @@ async def async_setup_entry( async_add_entities=async_add_entities, ) - async def async_send_ir_code_service(service): + async def async_send_ir_code_service(service: ServiceCall) -> None: """Set IR code as device state attribute.""" entity_ids = service.data.get(ATTR_ENTITY_ID) ir_code = service.data.get(ATTR_IR_CODE) @@ -98,17 +110,23 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): """Representation of the value of a MySensors Switch child node.""" @property - def current_power_w(self): + def current_power_w(self) -> float | None: """Return the current power usage in W.""" set_req = self.gateway.const.SetReq - return self._values.get(set_req.V_WATT) + value = self._values.get(set_req.V_WATT) + float_value: float | None = None + if value is not None: + with suppress(ValueError): + float_value = float(value) + + return float_value @property - def is_on(self): + def is_on(self) -> bool: """Return True if switch is on.""" return self._values.get(self.value_type) == STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1, ack=1 @@ -118,7 +136,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): self._values[self.value_type] = STATE_ON self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0, ack=1 @@ -132,18 +150,18 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity): class MySensorsIRSwitch(MySensorsSwitch): """IR switch child class to MySensorsSwitch.""" - def __init__(self, *args): + def __init__(self, *args: Any) -> None: """Set up instance attributes.""" super().__init__(*args) - self._ir_code = None + self._ir_code: str | None = None @property - def is_on(self): + def is_on(self) -> bool: """Return True if switch is on.""" set_req = self.gateway.const.SetReq return self._values.get(set_req.V_LIGHT) == STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the IR switch on.""" set_req = self.gateway.const.SetReq if ATTR_IR_CODE in kwargs: @@ -162,7 +180,7 @@ class MySensorsIRSwitch(MySensorsSwitch): # Turn off switch after switch was turned on await self.async_turn_off() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the IR switch off.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -173,7 +191,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self._values[set_req.V_LIGHT] = STATE_OFF self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Update the controller with the latest value from a sensor.""" await super().async_update() self._ir_code = self._values.get(self.value_type) diff --git a/mypy.ini b/mypy.ini index 7c2dbd38ccd..bada042219a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -517,6 +517,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mysensors.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.nam.*] check_untyped_defs = true disallow_incomplete_defs = true From 88b60a44ad44b44399c092e130f46ecf23746327 Mon Sep 17 00:00:00 2001 From: Andreas <28764847+andreas-amlabs@users.noreply.github.com> Date: Mon, 7 Jun 2021 16:14:45 +0200 Subject: [PATCH 216/750] Bump nad_receiver to version 0.2.0 (#51381) Co-authored-by: andreas-amlabs --- homeassistant/components/nad/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 063ceca0fd7..59d82acddf2 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -2,7 +2,7 @@ "domain": "nad", "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", - "requirements": ["nad_receiver==0.0.12"], + "requirements": ["nad_receiver==0.2.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a21fb12143a..3770f1d6155 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -981,7 +981,7 @@ mychevy==2.1.1 mycroftapi==2.0 # homeassistant.components.nad -nad_receiver==0.0.12 +nad_receiver==0.2.0 # homeassistant.components.keenetic_ndms2 ndms2_client==0.1.1 From 67d9dc78cb97a67239149baf3866d28cf6b9a531 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Jun 2021 01:57:44 +1000 Subject: [PATCH 217/750] Bump aio_georss_gdacs to 0.5 (#51577) --- homeassistant/components/gdacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 26743a69d68..65407e85848 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -3,7 +3,7 @@ "name": "Global Disaster Alert and Coordination System (GDACS)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gdacs", - "requirements": ["aio_georss_gdacs==0.4"], + "requirements": ["aio_georss_gdacs==0.5"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 3770f1d6155..7a3571e7bfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -132,7 +132,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.3 # homeassistant.components.gdacs -aio_georss_gdacs==0.4 +aio_georss_gdacs==0.5 # homeassistant.components.ambient_station aioambient==1.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98a3bbc5621..7ed3fd8ddfe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.3 # homeassistant.components.gdacs -aio_georss_gdacs==0.4 +aio_georss_gdacs==0.5 # homeassistant.components.ambient_station aioambient==1.2.4 From 1879a4acea5523958ac5e7f57835164193006d83 Mon Sep 17 00:00:00 2001 From: Rolf Berkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:08:34 +0200 Subject: [PATCH 218/750] Bump meteoalertapi to 0.2.0 (#51383) * Update manifest.json * Add version bump to requirements * Add version bump to requirements * Update manifest.json * Update manifest.json * Update requirements_all.txt --- homeassistant/components/meteoalarm/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 0888a8fa063..ffdd7d8f49d 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.1.6"], + "requirements": ["meteoalertapi==0.2.0"], "codeowners": ["@rolfberkenbosch"], - "iot_class": "local_polling" + "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a3571e7bfc..1d418b66829 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -939,7 +939,7 @@ mcstatus==6.0.0 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.1.6 +meteoalertapi==0.2.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 9ffdf9ea08a7b03df4db58cb311831203ec89f11 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 7 Jun 2021 19:16:47 +0200 Subject: [PATCH 219/750] Update builder to 2021.06.2 (#51582) --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 190c449cf3c..607af99fb51 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -115,7 +115,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.05.0 + uses: home-assistant/builder@2021.06.2 with: args: | $BUILD_ARGS \ @@ -167,7 +167,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.05.0 + uses: home-assistant/builder@2021.06.2 with: args: | $BUILD_ARGS \ From a383198c0c70d85e78f149274c4fdcd0c183fb46 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Jun 2021 19:36:34 +0200 Subject: [PATCH 220/750] Fully type switch entity component (#51586) --- homeassistant/components/switch/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index f9e9542701b..db103915fa4 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -24,8 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -# mypy: allow-untyped-defs, no-check-untyped-defs - DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) @@ -52,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the switch is on based on the statemachine. Async friendly. @@ -117,9 +115,9 @@ class SwitchEntity(ToggleEntity): class SwitchDevice(SwitchEntity): """Representation of a switch (for backwards compatibility).""" - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any) -> None: """Print deprecation warning.""" - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # type: ignore[call-arg] _LOGGER.warning( "SwitchDevice is deprecated, modify %s to extend SwitchEntity", cls.__name__, From a3146ad150084ee10ba1c3d62f77e5f1a509ca2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jun 2021 08:21:10 -1000 Subject: [PATCH 221/750] Fix loop in tod binary sensor (#51491) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tod/binary_sensor.py | 22 +- tests/components/tod/test_binary_sensor.py | 219 +++++++++++++++++- 2 files changed, 237 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 4fd9a3b8bf9..8264468e2e7 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -156,6 +156,26 @@ class TodSensor(BinarySensorEntity): self._time_after += self._after_offset self._time_before += self._before_offset + def _turn_to_next_day(self): + """Turn to to the next day.""" + if is_sun_event(self._after): + self._time_after = get_astral_event_next( + self.hass, self._after, self._time_after - self._after_offset + ) + self._time_after += self._after_offset + else: + # Offset is already there + self._time_after += timedelta(days=1) + + if is_sun_event(self._before): + self._time_before = get_astral_event_next( + self.hass, self._before, self._time_before - self._before_offset + ) + self._time_before += self._before_offset + else: + # Offset is already there + self._time_before += timedelta(days=1) + async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" self._calculate_boudary_time() @@ -182,7 +202,7 @@ class TodSensor(BinarySensorEntity): if now < self._time_before: self._next_update = self._time_before return - self._calculate_boudary_time() + self._turn_to_next_day() self._next_update = self._time_after @callback diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 8b63082c36c..ef8088d6aab 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -12,6 +12,8 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + @pytest.fixture(autouse=True) def mock_legacy_time(legacy_patchable_time): @@ -26,6 +28,13 @@ def setup_fixture(hass): hass.config.longitude = 18.98583 +@pytest.fixture(autouse=True) +def restore_timezone(hass): + """Make sure we change timezone.""" + yield + dt_util.set_default_time_zone(ORIG_TIMEZONE) + + async def test_setup(hass): """Test the setup.""" config = { @@ -863,6 +872,7 @@ async def test_sun_offset(hass): async def test_dst(hass): """Test sun event with offset.""" hass.config.time_zone = "CET" + dt_util.set_default_time_zone(dt_util.get_time_zone("CET")) test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -882,7 +892,210 @@ async def test_dst(hass): await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes["after"] == "2019-03-30T03:30:00+01:00" - assert state.attributes["before"] == "2019-03-30T03:40:00+01:00" - assert state.attributes["next_update"] == "2019-03-30T03:30:00+01:00" + assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" + assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" + assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" assert state.state == STATE_OFF + + +async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-10T22:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 11, 6, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-12T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T22:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Night", + "before": "06:00", + "after": "22:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-10T22:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): + """Test simple before after.""" + hass.config.time_zone = "UTC" + dt_util.set_default_time_zone(dt_util.UTC) + test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Morning", + "before": "08:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.morning") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T00:00:00+00:00" + assert state.attributes["before"] == "2019-01-11T08:00:00+00:00" + assert state.attributes["next_update"] == "2019-01-11T00:00:00+00:00" + + +async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "Europe/Berlin" + dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Dark", + "before": "06:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.dark") + assert state.state == STATE_OFF + assert state.attributes["after"] == "2019-01-11T00:00:00+01:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+01:00" + assert state.attributes["next_update"] == "2019-01-11T00:00:00+01:00" + + +async def test_simple_before_after_does_not_loop_berlin_in_range(hass): + """Test simple before after.""" + hass.config.time_zone = "Europe/Berlin" + dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + test_time = datetime(2019, 1, 10, 23, 43, 0, tzinfo=dt_util.UTC) + config = { + "binary_sensor": [ + { + "platform": "tod", + "name": "Dark", + "before": "06:00", + "after": "00:00", + } + ] + } + with patch( + "homeassistant.components.tod.binary_sensor.dt_util.utcnow", + return_value=test_time, + ): + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.dark") + assert state.state == STATE_ON + assert state.attributes["after"] == "2019-01-11T00:00:00+01:00" + assert state.attributes["before"] == "2019-01-11T06:00:00+01:00" + assert state.attributes["next_update"] == "2019-01-11T06:00:00+01:00" From ab2951f124b92ccbc035891d1476db83768f4f00 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 7 Jun 2021 19:21:24 +0100 Subject: [PATCH 222/750] AsusWRT fix keyerror when firmver is missing from info (#51499) Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/components/asuswrt/router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index e929ae80e26..6f22ddbbd6e 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -228,10 +228,10 @@ class AsusWrtRouter: # System model = await _get_nvram_info(self._api, "MODEL") - if model: + if model and "model" in model: self._model = model["model"] firmware = await _get_nvram_info(self._api, "FIRMWARE") - if firmware: + if firmware and "firmver" in firmware and "buildno" in firmware: self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" # Load tracked entities from registry From 3db21b407a0f8435c4a25b6d35ef5962d9c6a90c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Jun 2021 20:26:25 +0200 Subject: [PATCH 223/750] Add support for color_mode white to demo light (#51575) * Add support for color_mode white to demo light * Fix unique_id for newly added light * Update tests --- homeassistant/components/demo/light.py | 41 +++++++++++++------ tests/components/emulated_hue/test_hue_api.py | 4 ++ tests/components/google_assistant/__init__.py | 11 +++++ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 6680cd23874..4d9496ca10f 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -10,10 +10,12 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, + ATTR_WHITE, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, SUPPORT_EFFECT, LightEntity, ) @@ -27,6 +29,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP} +SUPPORT_DEMO_HS_WHITE = {COLOR_MODE_HS, COLOR_MODE_WHITE} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -72,6 +75,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= supported_color_modes={COLOR_MODE_RGBWW}, unique_id="light_5", ), + DemoLight( + available=True, + name="Entrance Color + White Lights", + hs_color=LIGHT_COLORS[1], + state=True, + supported_color_modes=SUPPORT_DEMO_HS_WHITE, + unique_id="light_6", + ), ] ) @@ -218,6 +229,20 @@ class DemoLight(LightEntity): """Turn the light on.""" self._state = True + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_COLOR_TEMP in kwargs: + self._color_mode = COLOR_MODE_COLOR_TEMP + self._ct = kwargs[ATTR_COLOR_TEMP] + + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT] + + if ATTR_HS_COLOR in kwargs: + self._color_mode = COLOR_MODE_HS + self._hs_color = kwargs[ATTR_HS_COLOR] + if ATTR_RGBW_COLOR in kwargs: self._color_mode = COLOR_MODE_RGBW self._rgbw_color = kwargs[ATTR_RGBW_COLOR] @@ -226,19 +251,9 @@ class DemoLight(LightEntity): self._color_mode = COLOR_MODE_RGBWW self._rgbww_color = kwargs[ATTR_RGBWW_COLOR] - if ATTR_HS_COLOR in kwargs: - self._color_mode = COLOR_MODE_HS - self._hs_color = kwargs[ATTR_HS_COLOR] - - if ATTR_COLOR_TEMP in kwargs: - self._color_mode = COLOR_MODE_COLOR_TEMP - self._ct = kwargs[ATTR_COLOR_TEMP] - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_EFFECT in kwargs: - self._effect = kwargs[ATTR_EFFECT] + if ATTR_WHITE in kwargs: + self._color_mode = COLOR_MODE_WHITE + self._brightness = kwargs[ATTR_WHITE] # As we have disabled polling, we need to inform # Home Assistant about updates in our state ourselves. diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index cb0b1f39365..f9df29e16ae 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -219,6 +219,10 @@ def hue_client(loop, hass_hue, aiohttp_client): "light.bed_light": {emulated_hue.CONF_ENTITY_HIDDEN: True}, # Kitchen light is explicitly excluded from being exposed "light.kitchen_lights": {emulated_hue.CONF_ENTITY_HIDDEN: True}, + # Entrance light is explicitly excluded from being exposed + "light.entrance_color_white_lights": { + emulated_hue.CONF_ENTITY_HIDDEN: True + }, # Ceiling Fan is explicitly excluded from being exposed "fan.ceiling_fan": {emulated_hue.CONF_ENTITY_HIDDEN: True}, # Expose the script diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 459b5bcadfc..a8b44511fb2 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -411,4 +411,15 @@ DEMO_DEVICES = [ "type": "action.devices.types.LIGHT", "willReportState": False, }, + { + "id": "light.entrance_color_white_lights", + "name": {"name": "Entrance Color + White Lights"}, + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Brightness", + "action.devices.traits.ColorSetting", + ], + "type": "action.devices.types.LIGHT", + "willReportState": False, + }, ] From ccf4b5a265b47d385ddd0158e5c3bcf2b03b752c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Jun 2021 10:12:33 -1000 Subject: [PATCH 224/750] Move remaining code out of netdisco to eliminate as SSDP dependency (#51588) --- .coveragerc | 1 + homeassistant/components/ssdp/descriptions.py | 9 ++-- homeassistant/components/ssdp/util.py | 42 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/ssdp/util.py diff --git a/.coveragerc b/.coveragerc index bb84fc73823..34e8d96aa17 100644 --- a/.coveragerc +++ b/.coveragerc @@ -973,6 +973,7 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/ssdp/util.py homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py diff --git a/homeassistant/components/ssdp/descriptions.py b/homeassistant/components/ssdp/descriptions.py index def302641ed..e754b10669a 100644 --- a/homeassistant/components/ssdp/descriptions.py +++ b/homeassistant/components/ssdp/descriptions.py @@ -6,10 +6,11 @@ import logging import aiohttp from defusedxml import ElementTree -from netdisco import util from homeassistant.core import HomeAssistant, callback +from .util import etree_to_dict + _LOGGER = logging.getLogger(__name__) @@ -64,7 +65,5 @@ class DescriptionManager: _LOGGER.debug("Error parsing %s: %s", xml_location, err) return None - parsed: dict[str, str] = ( - util.etree_to_dict(tree).get("root", {}).get("device", {}) - ) - return parsed + root = etree_to_dict(tree).get("root") or {} + return root.get("device") or {} diff --git a/homeassistant/components/ssdp/util.py b/homeassistant/components/ssdp/util.py new file mode 100644 index 00000000000..c28f8ce088d --- /dev/null +++ b/homeassistant/components/ssdp/util.py @@ -0,0 +1,42 @@ +"""Util functions used by SSDP.""" +from __future__ import annotations + +from collections import defaultdict +from typing import Any + +from defusedxml import ElementTree + + +# Adapted from http://stackoverflow.com/a/10077069 +# to follow the XML to JSON spec +# https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html +def etree_to_dict(tree: ElementTree) -> dict[str, dict[str, Any] | None]: + """Convert an ETree object to a dict.""" + # strip namespace + tag_name = tree.tag[tree.tag.find("}") + 1 :] + + tree_dict: dict[str, dict[str, Any] | None] = { + tag_name: {} if tree.attrib else None + } + children = list(tree) + if children: + child_dict: dict[str, list] = defaultdict(list) + for child in map(etree_to_dict, children): + for k, val in child.items(): + child_dict[k].append(val) + tree_dict = { + tag_name: {k: v[0] if len(v) == 1 else v for k, v in child_dict.items()} + } + dict_meta = tree_dict[tag_name] + if tree.attrib: + assert dict_meta is not None + dict_meta.update(("@" + k, v) for k, v in tree.attrib.items()) + if tree.text: + text = tree.text.strip() + if children or tree.attrib: + if text: + assert dict_meta is not None + dict_meta["#text"] = text + else: + tree_dict[tag_name] = text + return tree_dict From 76edfe6c62c7ad49206813f2231ff79c4d99e2bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Jun 2021 23:14:42 +0200 Subject: [PATCH 225/750] Fix deprecated value_template for MQTT light (#51587) --- homeassistant/components/mqtt/light/schema_basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 3e347363428..684dcf337aa 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -186,9 +186,6 @@ async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None ): """Set up a MQTT Light.""" - if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: - config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] - async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) @@ -236,6 +233,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: + config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] + topic = { key: config.get(key) for key in ( From aad90b864494d38746fd47fb0784789552589a0f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Jun 2021 00:03:33 +0200 Subject: [PATCH 226/750] Use supported color modes in Axis integration (#51557) * Use supported color modes in Axis integration * Fix Frencks comments * Do Frencks suggestion --- homeassistant/components/axis/light.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index e627d6ccdbd..17b48e7b232 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -4,7 +4,7 @@ from axis.event_stream import CLASS_LIGHT from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, LightEntity, ) from homeassistant.core import callback @@ -49,7 +49,8 @@ class AxisLight(AxisEventBase, LightEntity): self.current_intensity = 0 self.max_intensity = 0 - self._features = SUPPORT_BRIGHTNESS + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + self._attr_color_mode = COLOR_MODE_BRIGHTNESS async def async_added_to_hass(self) -> None: """Subscribe lights events.""" @@ -67,11 +68,6 @@ class AxisLight(AxisEventBase, LightEntity): ) self.max_intensity = max_intensity["data"]["ranges"][0]["high"] - @property - def supported_features(self): - """Flag supported features.""" - return self._features - @property def name(self): """Return the name of the light.""" From bc30920824790030a420369fafcf7c89e4ef83a3 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Mon, 7 Jun 2021 18:21:03 -0400 Subject: [PATCH 227/750] Correctly support use of Farenheit in Gree Climate component (#50260) --- homeassistant/components/gree/climate.py | 10 +++-- homeassistant/components/gree/const.py | 3 -- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_climate.py | 48 +++++++++++++++++---- 6 files changed, 48 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index e468195ff92..acd57ef590d 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -4,6 +4,10 @@ from __future__ import annotations import logging from greeclimate.device import ( + TEMP_MAX, + TEMP_MAX_F, + TEMP_MIN, + TEMP_MIN_F, FanSpeed, HorizontalSwing, Mode, @@ -55,8 +59,6 @@ from .const import ( DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, - MAX_TEMP, - MIN_TEMP, TARGET_TEMPERATURE_STEP, ) @@ -184,12 +186,12 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): @property def min_temp(self) -> float: """Return the minimum temperature supported by the device.""" - return MIN_TEMP + return TEMP_MIN if self.temperature_unit == TEMP_CELSIUS else TEMP_MIN_F @property def max_temp(self) -> float: """Return the maximum temperature supported by the device.""" - return MAX_TEMP + return TEMP_MAX if self.temperature_unit == TEMP_CELSIUS else TEMP_MAX_F @property def target_temperature_step(self) -> float: diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 2d9a48496b2..b4df7a1acde 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -16,9 +16,6 @@ COORDINATOR = "coordinator" FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_HIGH = "medium high" -MIN_TEMP = 16 -MAX_TEMP = 30 - MAX_ERRORS = 2 TARGET_TEMPERATURE_STEP = 1 diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 58ddb62216b..8108df18cc8 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.11.4"], + "requirements": ["greeclimate==0.11.7"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 1d418b66829..74547f648a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -702,7 +702,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.11.4 +greeclimate==0.11.7 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ed3fd8ddfe..b7016f86d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ google-nest-sdm==0.2.12 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.11.4 +greeclimate==0.11.7 # homeassistant.components.growatt_server growattServer==1.0.1 diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 62dd7ca545f..c062cfc5615 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -55,6 +55,8 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_UNAVAILABLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -376,26 +378,42 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): assert state.state == HVAC_MODE_OFF -async def test_send_target_temperature(hass, discovery, device, mock_now): +@pytest.mark.parametrize( + "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] +) +async def test_send_target_temperature(hass, discovery, device, units, temperature): """Test for sending target temperature command to the device.""" + hass.config.units.temperature_unit = units + if units == TEMP_FAHRENHEIT: + device().temperature_units = 1 + await async_setup_gree(hass) assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: temperature}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.attributes.get(ATTR_TEMPERATURE) == 25 + assert state.attributes.get(ATTR_TEMPERATURE) == temperature + + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS +@pytest.mark.parametrize( + "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] +) async def test_send_target_temperature_device_timeout( - hass, discovery, device, mock_now + hass, discovery, device, units, temperature ): """Test for sending target temperature command to the device with a device timeout.""" + hass.config.units.temperature_unit = units + if units == TEMP_FAHRENHEIT: + device().temperature_units = 1 device().push_state_update.side_effect = DeviceTimeoutError await async_setup_gree(hass) @@ -403,24 +421,36 @@ async def test_send_target_temperature_device_timeout( assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: temperature}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.attributes.get(ATTR_TEMPERATURE) == 25 + assert state.attributes.get(ATTR_TEMPERATURE) == temperature + + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS -async def test_update_target_temperature(hass, discovery, device, mock_now): +@pytest.mark.parametrize( + "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] +) +async def test_update_target_temperature(hass, discovery, device, units, temperature): """Test for updating target temperature from the device.""" - device().target_temperature = 32 + hass.config.units.temperature_unit = units + if units == TEMP_FAHRENHEIT: + device().temperature_units = 1 + device().target_temperature = temperature await async_setup_gree(hass) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.attributes.get(ATTR_TEMPERATURE) == 32 + assert state.attributes.get(ATTR_TEMPERATURE) == temperature + + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS @pytest.mark.parametrize( From 490c81aebc65970fdd2ed2d6ab67245056266575 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Jun 2021 08:24:54 +1000 Subject: [PATCH 228/750] Bump georss_qld_bushfire_alert_client to 0.5 (#51596) --- homeassistant/components/qld_bushfire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index aeddc8cbeb0..5b3de2cf62b 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -2,7 +2,7 @@ "domain": "qld_bushfire", "name": "Queensland Bushfire Alert", "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", - "requirements": ["georss_qld_bushfire_alert_client==0.3"], + "requirements": ["georss_qld_bushfire_alert_client==0.5"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 74547f648a0..5b2a196a826 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,7 +654,7 @@ georss_generic_client==0.4 georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire -georss_qld_bushfire_alert_client==0.3 +georss_qld_bushfire_alert_client==0.5 # homeassistant.components.huawei_lte # homeassistant.components.kef diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7016f86d9c..7b55156794c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ georss_generic_client==0.4 georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire -georss_qld_bushfire_alert_client==0.3 +georss_qld_bushfire_alert_client==0.5 # homeassistant.components.huawei_lte # homeassistant.components.kef From e257dd4d071b7537535e5c6dbd872d3af4334ef7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 17:29:17 -0500 Subject: [PATCH 229/750] Fix Sonos battery sensors on S1 firmware (#51585) --- homeassistant/components/sonos/speaker.py | 31 ++++++++++++----------- tests/components/sonos/test_sensor.py | 13 ++++------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 26a75f94065..d7eb6ea8358 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -173,7 +173,7 @@ class SonosSpeaker: self.zone_name = speaker_info["zone_name"] # Battery - self.battery_info: dict[str, Any] | None = None + self.battery_info: dict[str, Any] = {} self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None @@ -208,21 +208,15 @@ class SonosSpeaker: self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - if (battery_info := fetch_battery_info_or_none(self.soco)) is None: - self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) - else: + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info - # Only create a polling task if successful, may fail on S1 firmware - if battery_info: - # Battery events can be infrequent, polling is still necessary - self._battery_poll_timer = self.hass.helpers.event.track_time_interval( - self.async_poll_battery, BATTERY_SCAN_INTERVAL - ) - else: - _LOGGER.warning( - "S1 firmware detected, battery sensor may update infrequently" - ) + # Battery events can be infrequent, polling is still necessary + self._battery_poll_timer = self.hass.helpers.event.track_time_interval( + self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) + else: + self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) if new_alarms := self.update_alarms_for_speaker(): dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) @@ -386,7 +380,7 @@ class SonosSpeaker: async def async_update_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" - if (more_info := event.variables.get("more_info")) is not None: + if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) await self.async_update_battery_info(battery_dict) self.async_write_entity_states() @@ -515,12 +509,19 @@ class SonosSpeaker: if not self._battery_poll_timer: # Battery info received for an S1 speaker + new_battery = not self.battery_info self.battery_info.update( { "Level": int(battery_dict["BattPct"]), "PowerSource": "EXTERNAL" if is_charging else "BATTERY", } ) + if new_battery: + _LOGGER.warning( + "S1 firmware detected on %s, battery info may update infrequently", + self.zone_name, + ) + async_dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) return if is_charging == self.charging: diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index c8910b481f3..12c12821a0d 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -3,7 +3,7 @@ from pysonos.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -68,21 +68,18 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): entity_registry = await hass.helpers.entity_registry.async_get_registry() - battery = entity_registry.entities["sensor.zone_a_battery"] - battery_state = hass.states.get(battery.entity_id) - assert battery_state.state == STATE_UNAVAILABLE - - power = entity_registry.entities["binary_sensor.zone_a_power"] - power_state = hass.states.get(power.entity_id) - assert power_state.state == STATE_UNAVAILABLE + assert "sensor.zone_a_battery" not in entity_registry.entities + assert "binary_sensor.zone_a_power" not in entity_registry.entities # Update the speaker with a callback event sub_callback(battery_event) await hass.async_block_till_done() + battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" + power = entity_registry.entities["binary_sensor.zone_a_power"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" From 330f713e43d8e5b6554d4e6c1832fa149bc477c7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Jun 2021 00:21:17 +0000 Subject: [PATCH 230/750] [ci skip] Translation update --- .../components/airvisual/translations/he.json | 4 +++- .../ambient_station/translations/he.json | 1 + .../components/apple_tv/translations/he.json | 10 +++++++++- .../components/axis/translations/he.json | 5 +++++ .../binary_sensor/translations/he.json | 4 ++++ .../coronavirus/translations/he.json | 7 +++++++ .../devolo_home_control/translations/he.json | 3 +++ .../dialogflow/translations/he.json | 3 ++- .../components/fan/translations/he.json | 6 ++++++ .../flunearyou/translations/he.json | 3 +++ .../components/geofency/translations/he.json | 3 ++- .../components/gpslogger/translations/he.json | 1 + .../components/ifttt/translations/he.json | 1 + .../components/ipp/translations/he.json | 3 +++ .../islamic_prayer_times/translations/he.json | 7 +++++++ .../components/kodi/translations/he.json | 5 +++++ .../components/light/translations/he.json | 9 ++++++++- .../components/locative/translations/he.json | 6 ++++++ .../components/mailgun/translations/he.json | 1 + .../components/notion/translations/he.json | 3 +++ .../components/onewire/translations/he.json | 9 ++++++++- .../components/openuv/translations/he.json | 3 +++ .../components/owntracks/translations/he.json | 7 +++++++ .../philips_js/translations/he.json | 3 ++- .../components/plugwise/translations/he.json | 3 +++ .../components/profiler/translations/he.json | 12 +++++++++++ .../components/roku/translations/he.json | 1 + .../components/rpi_power/translations/he.json | 12 +++++++++++ .../components/sharkiq/translations/he.json | 2 ++ .../components/shelly/translations/he.json | 1 + .../somfy_mylink/translations/he.json | 3 +++ .../components/songpal/translations/he.json | 5 +++++ .../components/tesla/translations/he.json | 4 +++- .../components/vacuum/translations/he.json | 20 ++++++++++++++++--- .../components/vilfo/translations/he.json | 4 ++++ .../xiaomi_miio/translations/he.json | 3 ++- 36 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/islamic_prayer_times/translations/he.json create mode 100644 homeassistant/components/owntracks/translations/he.json create mode 100644 homeassistant/components/profiler/translations/he.json create mode 100644 homeassistant/components/rpi_power/translations/he.json diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index a5d64ed3a86..5dfc5cbdd73 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -12,7 +12,9 @@ "step": { "geography_by_coords": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" } }, "geography_by_name": { diff --git a/homeassistant/components/ambient_station/translations/he.json b/homeassistant/components/ambient_station/translations/he.json index 1ebf99c897c..f34e568aa2f 100644 --- a/homeassistant/components/ambient_station/translations/he.json +++ b/homeassistant/components/ambient_station/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" }, "error": { + "invalid_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df" }, "step": { diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 7173d6715e8..81e13ec1878 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -7,7 +7,10 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name}", "step": { @@ -15,6 +18,11 @@ "data": { "pin": "\u05e7\u05d5\u05d3 PIN" } + }, + "user": { + "data": { + "device_input": "\u05d4\u05ea\u05e7\u05df" + } } } } diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json index c0921a88332..903656d41cf 100644 --- a/homeassistant/components/axis/translations/he.json +++ b/homeassistant/components/axis/translations/he.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" @@ -11,6 +15,7 @@ "data": { "host": "\u05de\u05d0\u05e8\u05d7", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index df79548f4a8..b7375f3175b 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -2,10 +2,14 @@ "device_automation": { "condition_type": { "is_cold": "{entity_name} \u05e7\u05e8", + "is_light": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8", + "is_no_light": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8", "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8" }, "trigger_type": { "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "light": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", + "no_light": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8" } }, diff --git a/homeassistant/components/coronavirus/translations/he.json b/homeassistant/components/coronavirus/translations/he.json index f662edefc41..5ac1be49cfb 100644 --- a/homeassistant/components/coronavirus/translations/he.json +++ b/homeassistant/components/coronavirus/translations/he.json @@ -3,6 +3,13 @@ "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "country": "\u05de\u05d3\u05d9\u05e0\u05d4" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index 27b258cd756..903818bf429 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, diff --git a/homeassistant/components/dialogflow/translations/he.json b/homeassistant/components/dialogflow/translations/he.json index d0c3523da94..ebee9aee976 100644 --- a/homeassistant/components/dialogflow/translations/he.json +++ b/homeassistant/components/dialogflow/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } } } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/he.json b/homeassistant/components/fan/translations/he.json index e2081b7460e..63139b0fe34 100644 --- a/homeassistant/components/fan/translations/he.json +++ b/homeassistant/components/fan/translations/he.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json index f6195b11843..02a79d5fbcc 100644 --- a/homeassistant/components/flunearyou/translations/he.json +++ b/homeassistant/components/flunearyou/translations/he.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geofency/translations/he.json b/homeassistant/components/geofency/translations/he.json index d0c3523da94..ebee9aee976 100644 --- a/homeassistant/components/geofency/translations/he.json +++ b/homeassistant/components/geofency/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", + "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/he.json b/homeassistant/components/gpslogger/translations/he.json index 803c0e63ec3..ebee9aee976 100644 --- a/homeassistant/components/gpslogger/translations/he.json +++ b/homeassistant/components/gpslogger/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } } diff --git a/homeassistant/components/ifttt/translations/he.json b/homeassistant/components/ifttt/translations/he.json index 803c0e63ec3..ebee9aee976 100644 --- a/homeassistant/components/ifttt/translations/he.json +++ b/homeassistant/components/ifttt/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } } diff --git a/homeassistant/components/ipp/translations/he.json b/homeassistant/components/ipp/translations/he.json index df531a51b69..6c93f705a48 100644 --- a/homeassistant/components/ipp/translations/he.json +++ b/homeassistant/components/ipp/translations/he.json @@ -7,10 +7,13 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "flow_title": "{name}", "step": { "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } diff --git a/homeassistant/components/islamic_prayer_times/translations/he.json b/homeassistant/components/islamic_prayer_times/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/he.json b/homeassistant/components/kodi/translations/he.json index 162f1f86433..07d8838f200 100644 --- a/homeassistant/components/kodi/translations/he.json +++ b/homeassistant/components/kodi/translations/he.json @@ -26,6 +26,11 @@ "port": "\u05e4\u05ea\u05d7\u05d4", "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } + }, + "ws_port": { + "data": { + "ws_port": "\u05e4\u05ea\u05d7\u05d4" + } } } } diff --git a/homeassistant/components/light/translations/he.json b/homeassistant/components/light/translations/he.json index 9d4b3633088..a61237ba51e 100644 --- a/homeassistant/components/light/translations/he.json +++ b/homeassistant/components/light/translations/he.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "brightness_decrease": "\u05d4\u05e4\u05d7\u05ea \u05d0\u05ea \u05d4\u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05e9\u05dc {entity_name}", + "brightness_increase": "\u05d4\u05d2\u05d1\u05e8 \u05d0\u05ea \u05d4\u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05e9\u05dc {entity_name}", + "flash": "\u05d4\u05d1\u05d4\u05d1 \u05d0\u05ea {entity_name}", "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}", "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" @@ -8,12 +11,16 @@ "condition_type": { "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + }, + "trigger_type": { + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", - "on": "\u05d3\u05dc\u05d5\u05e7" + "on": "\u05de\u05d5\u05e4\u05e2\u05dc" } }, "title": "\u05d0\u05d5\u05b9\u05e8" diff --git a/homeassistant/components/locative/translations/he.json b/homeassistant/components/locative/translations/he.json index 803c0e63ec3..7e155c6bdd7 100644 --- a/homeassistant/components/locative/translations/he.json +++ b/homeassistant/components/locative/translations/he.json @@ -1,7 +1,13 @@ { "config": { "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/he.json b/homeassistant/components/mailgun/translations/he.json index 803c0e63ec3..ebee9aee976 100644 --- a/homeassistant/components/mailgun/translations/he.json +++ b/homeassistant/components/mailgun/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } } diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json index 837c2ac983b..1a397f894cf 100644 --- a/homeassistant/components/notion/translations/he.json +++ b/homeassistant/components/notion/translations/he.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/onewire/translations/he.json b/homeassistant/components/onewire/translations/he.json index 7f3951315a9..d83d1f76175 100644 --- a/homeassistant/components/onewire/translations/he.json +++ b/homeassistant/components/onewire/translations/he.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "step": { "owserver": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/openuv/translations/he.json b/homeassistant/components/openuv/translations/he.json index af4ec56b10c..90570023f05 100644 --- a/homeassistant/components/openuv/translations/he.json +++ b/homeassistant/components/openuv/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/owntracks/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/he.json b/homeassistant/components/philips_js/translations/he.json index c860b365bff..499f76c059e 100644 --- a/homeassistant/components/philips_js/translations/he.json +++ b/homeassistant/components/philips_js/translations/he.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "pairing_failure": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e9\u05d9\u05d9\u05da: {error_id}" + "pairing_failure": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e9\u05d9\u05d9\u05da: {error_id}", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "pair": { diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index a38d2a24118..a89120b85ab 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/profiler/translations/he.json b/homeassistant/components/profiler/translations/he.json new file mode 100644 index 00000000000..08506bf3437 --- /dev/null +++ b/homeassistant/components/profiler/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/he.json b/homeassistant/components/roku/translations/he.json index 651135370ba..41d59c29fd8 100644 --- a/homeassistant/components/roku/translations/he.json +++ b/homeassistant/components/roku/translations/he.json @@ -8,6 +8,7 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/rpi_power/translations/he.json b/homeassistant/components/rpi_power/translations/he.json new file mode 100644 index 00000000000..a18f311e43a --- /dev/null +++ b/homeassistant/components/rpi_power/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/he.json b/homeassistant/components/sharkiq/translations/he.json index 684220fe307..2ab1819698c 100644 --- a/homeassistant/components/sharkiq/translations/he.json +++ b/homeassistant/components/sharkiq/translations/he.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/shelly/translations/he.json b/homeassistant/components/shelly/translations/he.json index 46ab05b4045..44d5897f85d 100644 --- a/homeassistant/components/shelly/translations/he.json +++ b/homeassistant/components/shelly/translations/he.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name}", diff --git a/homeassistant/components/somfy_mylink/translations/he.json b/homeassistant/components/somfy_mylink/translations/he.json index f47bfa240ba..e218ba687a4 100644 --- a/homeassistant/components/somfy_mylink/translations/he.json +++ b/homeassistant/components/somfy_mylink/translations/he.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json index 329263d1bc3..a8c8d1d0294 100644 --- a/homeassistant/components/songpal/translations/he.json +++ b/homeassistant/components/songpal/translations/he.json @@ -10,6 +10,11 @@ "step": { "init": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" + }, + "user": { + "data": { + "endpoint": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05e7\u05e6\u05d4" + } } } } diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json index fe406357051..9f3eeb2fc21 100644 --- a/homeassistant/components/tesla/translations/he.json +++ b/homeassistant/components/tesla/translations/he.json @@ -5,7 +5,9 @@ "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { "user": { diff --git a/homeassistant/components/vacuum/translations/he.json b/homeassistant/components/vacuum/translations/he.json index dc6b5da01cb..82e0d073406 100644 --- a/homeassistant/components/vacuum/translations/he.json +++ b/homeassistant/components/vacuum/translations/he.json @@ -1,14 +1,28 @@ { + "device_automation": { + "action_type": { + "clean": "\u05d0\u05e4\u05e9\u05e8 \u05dc-{entity_name} \u05dc\u05e0\u05e7\u05d5\u05ea", + "dock": "\u05d0\u05e4\u05e9\u05e8 \u05dc-{entity_name} \u05dc\u05d7\u05d6\u05d5\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u05de\u05e0\u05e7\u05d4", + "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + }, + "trigger_type": { + "cleaning": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", + "docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + } + }, "state": { "_": { "cleaning": "\u05de\u05e0\u05e7\u05d4", - "docked": "\u05d1\u05e2\u05d2\u05d9\u05e0\u05d4", + "docked": "\u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", "error": "\u05e9\u05d2\u05d9\u05d0\u05d4", "idle": "\u05de\u05de\u05ea\u05d9\u05df", - "off": "\u05de\u05db\u05d5\u05d1\u05d4", + "off": "\u05db\u05d1\u05d5\u05d9", "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "paused": "\u05de\u05d5\u05e9\u05d4\u05d4", - "returning": "\u05d7\u05d6\u05d5\u05e8 \u05dc\u05e2\u05d2\u05d9\u05e0\u05d4" + "returning": "\u05d7\u05d5\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" } }, "title": "\u05e9\u05d5\u05d0\u05d1 \u05d0\u05d1\u05e7" diff --git a/homeassistant/components/vilfo/translations/he.json b/homeassistant/components/vilfo/translations/he.json index 54110684daf..642f482d14e 100644 --- a/homeassistant/components/vilfo/translations/he.json +++ b/homeassistant/components/vilfo/translations/he.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 79e2371c033..f7f60416ade 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -19,7 +19,8 @@ "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - } + }, + "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara." } } } From 6c2e452e3d0ec096a6b94a1840eee65ffdc8668e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Jun 2021 03:11:17 +0200 Subject: [PATCH 231/750] Fix kraken I/O and sleep in tests (#51599) --- homeassistant/components/kraken/__init__.py | 5 +- .../components/kraken/config_flow.py | 2 +- tests/components/kraken/conftest.py | 11 +++ tests/components/kraken/test_config_flow.py | 75 ++++++++----------- 4 files changed, 47 insertions(+), 46 deletions(-) create mode 100644 tests/components/kraken/conftest.py diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 12b9e51d2d6..76a4976f163 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -25,6 +25,8 @@ from .const import ( ) from .utils import get_tradable_asset_pairs +CALL_RATE_LIMIT_SLEEP = 1 + PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) @@ -125,7 +127,8 @@ class KrakenData: self._config_entry, options=options ) await self._async_refresh_tradable_asset_pairs() - await asyncio.sleep(1) # Wait 1 second to avoid triggering the CallRateLimiter + # Wait 1 second to avoid triggering the CallRateLimiter + await asyncio.sleep(CALL_RATE_LIMIT_SLEEP) self.coordinator = DataUpdateCoordinator( self._hass, _LOGGER, diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index a34bf78557e..a619fb1e962 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -38,7 +38,7 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" - if DOMAIN in self.hass.data: + if self._async_current_entries(): return self.async_abort(reason="already_configured") if user_input is not None: return self.async_create_entry(title=DOMAIN, data=user_input) diff --git a/tests/components/kraken/conftest.py b/tests/components/kraken/conftest.py new file mode 100644 index 00000000000..f34dedc4df9 --- /dev/null +++ b/tests/components/kraken/conftest.py @@ -0,0 +1,11 @@ +"""Provide common pytest fixtures for kraken tests.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_call_rate_limit_sleep(): + """Patch the call rate limit sleep time.""" + with patch("homeassistant.components.kraken.CALL_RATE_LIMIT_SLEEP", new=0): + yield diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index 1a09fbe92c6..6015098e573 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -12,74 +12,61 @@ from tests.common import MockConfigEntry async def test_config_flow(hass): """Test we can finish a config flow.""" with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, - ): + "homeassistant.components.kraken.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} ) assert result["type"] == "form" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" await hass.async_block_till_done() - state = hass.states.get("sensor.xbt_usd_ask") - assert state + + assert result["type"] == "create_entry" + assert len(mock_setup_entry.mock_calls) == 1 async def test_already_configured(hass): """Test we can not add a second config flow.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == "create_entry" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_options(hass): """Test options for Kraken.""" + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: 60, + CONF_TRACKED_ASSET_PAIRS: [ + "ADA/XBT", + "ADA/ETH", + "XBT/EUR", + "XBT/GBP", + "XBT/USD", + "XBT/JPY", + ], + }, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", return_value=TRADEABLE_ASSET_PAIR_RESPONSE, ), patch( "pykrakenapi.KrakenAPI.get_ticker_information", return_value=TICKER_INFORMATION_RESPONSE, ): - entry = MockConfigEntry( - domain=DOMAIN, - options={ - CONF_SCAN_INTERVAL: 60, - CONF_TRACKED_ASSET_PAIRS: [ - "ADA/XBT", - "ADA/ETH", - "XBT/EUR", - "XBT/GBP", - "XBT/USD", - "XBT/JPY", - ], - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From de2dc92741d2e61a6422897433ddf37540643232 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 20:17:14 -0500 Subject: [PATCH 232/750] Handle missing section ID for Plex clips (#51598) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 2dc7b83b439..406c263dbff 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -78,7 +78,7 @@ class PlexSession: if media.librarySectionID in SPECIAL_SECTIONS: self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID] - elif media.librarySectionID < 1: + elif media.librarySectionID and media.librarySectionID < 1: self.media_library_title = UNKNOWN_SECTION _LOGGER.warning( "Unknown library section ID (%s) for title '%s', please create an issue", From ae86e96d347fa6d1bac10b62e8abe0c76ec3d2c1 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 7 Jun 2021 21:23:44 -0400 Subject: [PATCH 233/750] Fix misaligned high/low temperatures in weather card (#49826) --- homeassistant/components/environment_canada/weather.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 9abbc33bc93..5301843fc0a 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -202,16 +202,17 @@ def get_forecast(ec_data, forecast_type): ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]), } ) + half_days = half_days[2:] else: today.update( { + ATTR_FORECAST_TEMP: None, ATTR_FORECAST_TEMP_LOW: int(half_days[0]["temperature"]), - ATTR_FORECAST_TEMP: int(half_days[1]["temperature"]), } ) + half_days = half_days[1:] forecast_array.append(today) - half_days = half_days[2:] for day, high, low in zip(range(1, 6), range(0, 9, 2), range(1, 10, 2)): forecast_array.append( From d0a8e27036dc29ca80eb483fc904b3022e3b7b99 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 8 Jun 2021 03:28:31 +0200 Subject: [PATCH 234/750] Add Rituals number platform (#49723) --- .coveragerc | 1 + .../rituals_perfume_genie/__init__.py | 2 +- .../components/rituals_perfume_genie/const.py | 2 + .../rituals_perfume_genie/number.py | 126 ++++++++++++++++++ .../rituals_perfume_genie/switch.py | 4 +- 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/rituals_perfume_genie/number.py diff --git a/.coveragerc b/.coveragerc index 34e8d96aa17..9437f8943a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -846,6 +846,7 @@ omit = homeassistant/components/ripple/sensor.py homeassistant/components/rituals_perfume_genie/binary_sensor.py homeassistant/components/rituals_perfume_genie/entity.py + homeassistant/components/rituals_perfume_genie/number.py homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 0ec0ca47a09..13699dcd64b 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "number", "sensor", "switch"] EMPTY_CREDENTIALS = "" diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index c0bf72fb90e..5ba687c3d7c 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -8,4 +8,6 @@ ACCOUNT_HASH = "account_hash" ATTRIBUTES = "attributes" HUBLOT = "hublot" ID = "id" +ROOM = "roomc" SENSORS = "sensors" +SPEED = "speedc" diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py new file mode 100644 index 00000000000..d7f8a3677b7 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -0,0 +1,126 @@ +"""Support for Rituals Perfume Genie numbers.""" +from __future__ import annotations + +import logging + +from pyrituals import Diffuser + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RitualsDataUpdateCoordinator +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM, SPEED +from .entity import DiffuserEntity + +_LOGGER = logging.getLogger(__name__) + +MIN_PERFUME_AMOUNT = 1 +MAX_PERFUME_AMOUNT = 3 +MIN_ROOM_SIZE = 1 +MAX_ROOM_SIZE = 4 + +PERFUME_AMOUNT_SUFFIX = " Perfume Amount" +ROOM_SIZE_SUFFIX = " Room Size" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the diffuser numbers.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities: list[DiffuserEntity] = [] + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserPerfumeAmount(diffuser, coordinator)) + entities.append(DiffuserRoomSize(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserPerfumeAmount(NumberEntity, DiffuserEntity): + """Representation of a diffuser perfume amount number.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the diffuser perfume amount number.""" + super().__init__(diffuser, coordinator, PERFUME_AMOUNT_SUFFIX) + + @property + def icon(self) -> str: + """Return the icon of the perfume amount entity.""" + return "mdi:gauge" + + @property + def value(self) -> int: + """Return the current perfume amount.""" + return self._diffuser.hub_data[ATTRIBUTES][SPEED] + + @property + def min_value(self) -> int: + """Return the minimum perfume amount.""" + return MIN_PERFUME_AMOUNT + + @property + def max_value(self) -> int: + """Return the maximum perfume amount.""" + return MAX_PERFUME_AMOUNT + + async def async_set_value(self, value: float) -> None: + """Set the perfume amount.""" + if value.is_integer() and MIN_PERFUME_AMOUNT <= value <= MAX_PERFUME_AMOUNT: + await self._diffuser.set_perfume_amount(int(value)) + else: + _LOGGER.warning( + "Can't set the perfume amount to %s. Perfume amount must be an integer between %s and %s, inclusive", + value, + MIN_PERFUME_AMOUNT, + MAX_PERFUME_AMOUNT, + ) + + +class DiffuserRoomSize(NumberEntity, DiffuserEntity): + """Representation of a diffuser room size number.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the diffuser room size number.""" + super().__init__(diffuser, coordinator, ROOM_SIZE_SUFFIX) + + @property + def icon(self) -> str: + """Return the icon of the room size entity.""" + return "mdi:ruler-square" + + @property + def value(self) -> int: + """Return the current room size.""" + return self._diffuser.hub_data[ATTRIBUTES][ROOM] + + @property + def min_value(self) -> int: + """Return the minimum room size.""" + return MIN_ROOM_SIZE + + @property + def max_value(self) -> int: + """Return the maximum room size.""" + return MAX_ROOM_SIZE + + async def async_set_value(self, value: float) -> None: + """Set the room size.""" + if value.is_integer() and MIN_ROOM_SIZE <= value <= MAX_ROOM_SIZE: + await self._diffuser.set_room_size(int(value)) + else: + _LOGGER.warning( + "Can't set the room size to %s. Room size must be an integer between %s and %s, inclusive", + value, + MIN_ROOM_SIZE, + MAX_ROOM_SIZE, + ) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index a2ca89dc2ac..54e985af77f 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -11,12 +11,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM, SPEED from .entity import DiffuserEntity FAN = "fanc" -SPEED = "speedc" -ROOM = "roomc" ON_STATE = "1" From 4ffa0dd1996cc7d66e207bbb430c6d37d9206f1a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 20:51:42 -0500 Subject: [PATCH 235/750] Detect Sonos reboots and recreate subscriptions (#51377) --- homeassistant/components/sonos/__init__.py | 89 +++++++++++++++------- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/speaker.py | 26 ++++++- 3 files changed, 86 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d26d7d0c47a..4bb9b475b9a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict, deque import datetime +from enum import Enum import logging import socket from urllib.parse import urlparse @@ -26,7 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from .const import ( DATA_SONOS, @@ -35,6 +36,7 @@ from .const import ( PLATFORMS, SONOS_ALARM_UPDATE, SONOS_GROUP_UPDATE, + SONOS_REBOOTED, SONOS_SEEN, UPNP_ST, ) @@ -67,6 +69,14 @@ CONFIG_SCHEMA = vol.Schema( ) +class SoCoCreationSource(Enum): + """Represent the creation source of a SoCo instance.""" + + CONFIGURED = "configured" + DISCOVERED = "discovered" + REBOOTED = "rebooted" + + class SonosData: """Storage class for platform global data.""" @@ -80,6 +90,7 @@ class SonosData: self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None self.ssdp_known: set[str] = set() + self.boot_counts: dict[str, int] = {} async def async_setup(hass, config): @@ -129,7 +140,6 @@ async def async_setup_entry( # noqa: C901 def _discovered_player(soco: SoCo) -> None: """Handle a (re)discovered player.""" try: - _LOGGER.debug("Reached _discovered_player, soco=%s", soco) speaker_info = soco.get_speaker_info(True) _LOGGER.debug("Adding new speaker: %s", speaker_info) speaker = SonosSpeaker(hass, soco, speaker_info) @@ -143,22 +153,40 @@ async def async_setup_entry( # noqa: C901 except SoCoException as ex: _LOGGER.debug("SoCoException, ex=%s", ex) + def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: + """Create a soco instance and return if successful.""" + try: + soco = pysonos.SoCo(ip_address) + # Ensure that the player is available and UID is cached + _ = soco.uid + _ = soco.volume + return soco + except (OSError, SoCoException) as ex: + _LOGGER.warning( + "Failed to connect to %s player '%s': %s", source.value, ip_address, ex + ) + return None + def _manual_hosts(now: datetime.datetime | None = None) -> None: """Players from network configuration.""" for host in hosts: - try: - _LOGGER.debug("Testing %s", host) - player = pysonos.SoCo(socket.gethostbyname(host)) - if player.is_visible: - # Make sure that the player is available - _ = player.volume - _discovered_player(player) - except (OSError, SoCoException) as ex: - _LOGGER.debug("Issue connecting to '%s': %s", host, ex) - if now is None: - _LOGGER.warning("Failed to initialize '%s'", host) + ip_addr = socket.gethostbyname(host) + known_uid = next( + ( + uid + for uid, speaker in data.discovered.items() + if speaker.soco.ip_address == ip_addr + ), + None, + ) + + if known_uid: + dispatcher_send(hass, f"{SONOS_SEEN}-{known_uid}") + else: + soco = _create_soco(ip_addr, SoCoCreationSource.CONFIGURED) + if soco and soco.is_visible: + _discovered_player(soco) - _LOGGER.debug("Tested all hosts") data.hosts_heartbeat = hass.helpers.event.call_later( DISCOVERY_INTERVAL.total_seconds(), _manual_hosts ) @@ -168,32 +196,41 @@ async def async_setup_entry( # noqa: C901 async_dispatcher_send(hass, SONOS_GROUP_UPDATE) def _discovered_ip(ip_address): - try: - player = pysonos.SoCo(ip_address) - except (OSError, SoCoException): - _LOGGER.debug("Failed to connect to discovered player '%s'", ip_address) - return - if player.is_visible: - _discovered_player(player) + soco = _create_soco(ip_address, SoCoCreationSource.DISCOVERED) + if soco and soco.is_visible: + _discovered_player(soco) - async def _async_create_discovered_player(uid, discovered_ip): + async def _async_create_discovered_player(uid, discovered_ip, boot_seqnum): """Only create one player at a time.""" async with discovery_lock: - if uid in data.discovered: - async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}") + if uid not in data.discovered: + await hass.async_add_executor_job(_discovered_ip, discovered_ip) return - await hass.async_add_executor_job(_discovered_ip, discovered_ip) + + if boot_seqnum and boot_seqnum > data.boot_counts[uid]: + data.boot_counts[uid] = boot_seqnum + if soco := await hass.async_add_executor_job( + _create_soco, discovered_ip, SoCoCreationSource.REBOOTED + ): + async_dispatcher_send(hass, f"{SONOS_REBOOTED}-{uid}", soco) + else: + async_dispatcher_send(hass, f"{SONOS_SEEN}-{uid}") @callback def _async_discovered_player(info): uid = info.get(ssdp.ATTR_UPNP_UDN) if uid.startswith("uuid:"): uid = uid[5:] + if boot_seqnum := info.get("X-RINCON-BOOTSEQ"): + boot_seqnum = int(boot_seqnum) + data.boot_counts.setdefault(uid, boot_seqnum) if uid not in data.ssdp_known: _LOGGER.debug("New discovery: %s", info) data.ssdp_known.add(uid) discovered_ip = urlparse(info[ssdp.ATTR_SSDP_LOCATION]).hostname - asyncio.create_task(_async_create_discovered_player(uid, discovered_ip)) + asyncio.create_task( + _async_create_discovered_player(uid, discovered_ip, boot_seqnum) + ) @callback def _async_signal_update_alarms(event): diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index d32dda6a53b..84ccb99baed 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -143,6 +143,7 @@ SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_ALARM_UPDATE = "sonos_alarm_update" SONOS_STATE_UPDATED = "sonos_state_updated" +SONOS_REBOOTED = "sonos_rebooted" SONOS_SEEN = "sonos_seen" SOURCE_LINEIN = "Line-in" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index d7eb6ea8358..47c9af62761 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -47,6 +47,7 @@ from .const import ( SONOS_ENTITY_CREATED, SONOS_GROUP_UPDATE, SONOS_POLL_UPDATE, + SONOS_REBOOTED, SONOS_SEEN, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, @@ -164,6 +165,7 @@ class SonosSpeaker: # Dispatcher handles self._entity_creation_dispatcher: Callable | None = None self._group_dispatcher: Callable | None = None + self._reboot_dispatcher: Callable | None = None self._seen_dispatcher: Callable | None = None # Device information @@ -207,6 +209,9 @@ class SonosSpeaker: self._seen_dispatcher = dispatcher_connect( self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) + self._reboot_dispatcher = dispatcher_connect( + self.hass, f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted + ) if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info @@ -449,10 +454,10 @@ class SonosSpeaker: self.async_write_entity_states() - async def async_unseen(self, now: datetime.datetime | None = None) -> None: + async def async_unseen( + self, now: datetime.datetime | None = None, will_reconnect: bool = False + ) -> None: """Make this player unavailable when it was not seen recently.""" - self.async_write_entity_states() - if self._seen_timer: self._seen_timer() self._seen_timer = None @@ -465,7 +470,20 @@ class SonosSpeaker: await subscription.unsubscribe() self._subscriptions = [] - self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid) + + if not will_reconnect: + self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid) + self.async_write_entity_states() + + async def async_rebooted(self, soco: SoCo) -> None: + """Handle a detected speaker reboot.""" + _LOGGER.warning( + "%s rebooted or lost network connectivity, reconnecting with %s", + self.zone_name, + soco, + ) + await self.async_unseen(will_reconnect=True) + await self.async_seen(soco) # # Alarm management From 51fa28aac3e4a30c6172806f1353636c9c89e6e8 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Jun 2021 15:36:23 +1000 Subject: [PATCH 236/750] Bump aio_geojson_geonetnz_volcano to v0.6 (#51602) --- homeassistant/components/geonetnz_volcano/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index ed0ebccf620..dbd793c49b3 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Volcano", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", - "requirements": ["aio_geojson_geonetnz_volcano==0.5"], + "requirements": ["aio_geojson_geonetnz_volcano==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5b2a196a826..14b38e89d83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -126,7 +126,7 @@ agent-py==0.0.23 aio_geojson_geonetnz_quakes==0.12 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.5 +aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b55156794c..8404675bea3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -66,7 +66,7 @@ agent-py==0.0.23 aio_geojson_geonetnz_quakes==0.12 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.5 +aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.3 From 01d4140177831b213da3fe7873e34e4453576967 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Tue, 8 Jun 2021 01:22:50 -0500 Subject: [PATCH 237/750] Modern Forms integration initial pass - Fan (#51317) * Modern Forms integration initial pass * cleanup of typing and nits * Stripped PR down to Fan only * Review cleanup * Set sleep_time to be required for service * Adjust minimum sleep time to one minute. * Code review changes * cleanup icon init a little --- CODEOWNERS | 1 + .../components/modern_forms/__init__.py | 176 +++++++++++++++ .../components/modern_forms/config_flow.py | 120 ++++++++++ .../components/modern_forms/const.py | 30 +++ homeassistant/components/modern_forms/fan.py | 180 +++++++++++++++ .../components/modern_forms/manifest.json | 17 ++ .../components/modern_forms/services.yaml | 28 +++ .../components/modern_forms/strings.json | 28 +++ .../modern_forms/translations/en.json | 28 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/modern_forms/__init__.py | 65 ++++++ .../modern_forms/test_config_flow.py | 198 ++++++++++++++++ tests/components/modern_forms/test_fan.py | 213 ++++++++++++++++++ tests/components/modern_forms/test_init.py | 60 +++++ tests/fixtures/modern_forms/device_info.json | 15 ++ .../modern_forms/device_info_no_light.json | 14 ++ .../fixtures/modern_forms/device_status.json | 17 ++ .../modern_forms/device_status_no_light.json | 14 ++ 21 files changed, 1217 insertions(+) create mode 100644 homeassistant/components/modern_forms/__init__.py create mode 100644 homeassistant/components/modern_forms/config_flow.py create mode 100644 homeassistant/components/modern_forms/const.py create mode 100644 homeassistant/components/modern_forms/fan.py create mode 100644 homeassistant/components/modern_forms/manifest.json create mode 100644 homeassistant/components/modern_forms/services.yaml create mode 100644 homeassistant/components/modern_forms/strings.json create mode 100644 homeassistant/components/modern_forms/translations/en.json create mode 100644 tests/components/modern_forms/__init__.py create mode 100644 tests/components/modern_forms/test_config_flow.py create mode 100644 tests/components/modern_forms/test_fan.py create mode 100644 tests/components/modern_forms/test_init.py create mode 100644 tests/fixtures/modern_forms/device_info.json create mode 100644 tests/fixtures/modern_forms/device_info_no_light.json create mode 100644 tests/fixtures/modern_forms/device_status.json create mode 100644 tests/fixtures/modern_forms/device_status_no_light.json diff --git a/CODEOWNERS b/CODEOWNERS index 405c624c22d..6471d547be3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -300,6 +300,7 @@ homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik +homeassistant/components/modern_forms/* @wonderslug homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/motion_blinds/* @starkillerOG diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py new file mode 100644 index 00000000000..7530f7658c7 --- /dev/null +++ b/homeassistant/components/modern_forms/__init__.py @@ -0,0 +1,176 @@ +"""The Modern Forms integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +from aiomodernforms import ( + ModernFormsConnectionError, + ModernFormsDevice, + ModernFormsError, +) +from aiomodernforms.models import Device as ModernFormsDeviceState + +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=5) +PLATFORMS = [ + FAN_DOMAIN, +] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Modern Forms device from a config entry.""" + + # Create Modern Forms instance for this entry + coordinator = ModernFormsDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=coordinator.data.info.mac_address + ) + + # Set up all platforms for this device/entry. + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Modern Forms config entry.""" + + # Unload entities for this entry/device. + unload_ok = all( + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ) + ) + ) + + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return unload_ok + + +def modernforms_exception_handler(func): + """Decorate Modern Forms calls to handle Modern Forms exceptions. + + A decorator that wraps the passed in function, catches Modern Forms errors, + and handles the availability of the device in the data coordinator. + """ + + async def handler(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + self.coordinator.update_listeners() + + except ModernFormsConnectionError as error: + _LOGGER.error("Error communicating with API: %s", error) + self.coordinator.last_update_success = False + self.coordinator.update_listeners() + + except ModernFormsError as error: + _LOGGER.error("Invalid response from API: %s", error) + + return handler + + +class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceState]): + """Class to manage fetching Modern Forms data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + *, + host: str, + ) -> None: + """Initialize global Modern Forms data updater.""" + self.modernforms = ModernFormsDevice( + host, session=async_get_clientsession(hass) + ) + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def update_listeners(self) -> None: + """Call update on all listeners.""" + for update_callback in self._listeners: + update_callback() + + async def _async_update_data(self) -> ModernFormsDevice: + """Fetch data from Modern Forms.""" + try: + return await self.modernforms.update( + full_update=not self.last_update_success + ) + except ModernFormsError as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error + + +class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]): + """Defines a Modern Forms device entity.""" + + coordinator: ModernFormsDataUpdateCoordinator + + def __init__( + self, + *, + entry_id: str, + coordinator: ModernFormsDataUpdateCoordinator, + name: str, + icon: str | None = None, + enabled_default: bool = True, + ) -> None: + """Initialize the Modern Forms entity.""" + super().__init__(coordinator) + self._attr_enabled_default = enabled_default + self._entry_id = entry_id + self._attr_icon = icon + self._attr_name = name + self._unsub_dispatcher = None + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Modern Forms device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, # type: ignore + ATTR_NAME: self.coordinator.data.info.device_name, + ATTR_MANUFACTURER: "Modern Forms", + ATTR_MODEL: self.coordinator.data.info.fan_type, + ATTR_SW_VERSION: f"{self.coordinator.data.info.firmware_version} / {self.coordinator.data.info.main_mcu_firmware_version}", + } diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py new file mode 100644 index 00000000000..67eb9cef0e4 --- /dev/null +++ b/homeassistant/components/modern_forms/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for Modern Forms.""" +from __future__ import annotations + +from typing import Any + +from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice +import voluptuous as vol + +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_POLL, + SOURCE_ZEROCONF, + ConfigFlow, +) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import DOMAIN + + +class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a ModernForms config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle setup by user for Modern Forms integration.""" + return await self._handle_config_flow(user_input) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle zeroconf discovery.""" + host = discovery_info["hostname"].rstrip(".") + name, _ = host.rsplit(".") + + self.context.update( + { + CONF_HOST: discovery_info["host"], + CONF_NAME: name, + CONF_MAC: discovery_info["properties"].get(CONF_MAC), + "title_placeholders": {"name": name}, + } + ) + + # Prepare configuration flow + return await self._handle_config_flow(discovery_info, True) + + async def async_step_zeroconf_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by zeroconf.""" + return await self._handle_config_flow(user_input) + + async def _handle_config_flow( + self, user_input: dict[str, Any] | None = None, prepare: bool = False + ) -> FlowResult: + """Config flow handler for ModernForms.""" + source = self.context.get("source") + + # Request user input, unless we are preparing discovery flow + if user_input is None: + user_input = {} + if not prepare: + if source == SOURCE_ZEROCONF: + return self._show_confirm_dialog() + return self._show_setup_form() + + if source == SOURCE_ZEROCONF: + user_input[CONF_HOST] = self.context.get(CONF_HOST) + user_input[CONF_MAC] = self.context.get(CONF_MAC) + + if user_input.get(CONF_MAC) is None or not prepare: + session = async_get_clientsession(self.hass) + device = ModernFormsDevice(user_input[CONF_HOST], session=session) + try: + device = await device.update() + except ModernFormsConnectionError: + if source == SOURCE_ZEROCONF: + return self.async_abort(reason="cannot_connect") + return self._show_setup_form({"base": "cannot_connect"}) + user_input[CONF_MAC] = device.info.mac_address + user_input[CONF_NAME] = device.info.device_name + + # Check if already configured + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) + + title = device.info.device_name + if source == SOURCE_ZEROCONF: + title = self.context.get(CONF_NAME) + + if prepare: + return await self.async_step_zeroconf_confirm() + + return self.async_create_entry( + title=title, + data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, + ) + + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) + + def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResult: + """Show the confirm dialog to the user.""" + name = self.context.get(CONF_NAME) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": name}, + errors=errors or {}, + ) diff --git a/homeassistant/components/modern_forms/const.py b/homeassistant/components/modern_forms/const.py new file mode 100644 index 00000000000..60791d97e64 --- /dev/null +++ b/homeassistant/components/modern_forms/const.py @@ -0,0 +1,30 @@ +"""Constants for the Modern Forms integration.""" + +DOMAIN = "modern_forms" + +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_OWNER = "owner" +ATTR_IDENTITY = "identity" +ATTR_MCU_FIRMWARE_VERSION = "mcu_firmware_version" +ATTR_FIRMWARE_VERSION = "firmware_version" + +SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}" +SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}" +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}" + +CONF_ON_UNLOAD = "ON_UNLOAD" + +OPT_BRIGHTNESS = "brightness" +OPT_ON = "on" +OPT_SPEED = "speed" + +# Services +SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer" +SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer" +SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer" +SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer" + +ATTR_SLEEP_TIME = "sleep_time" +CLEAR_TIMER = 0 diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py new file mode 100644 index 00000000000..86c68df6eee --- /dev/null +++ b/homeassistant/components/modern_forms/fan.py @@ -0,0 +1,180 @@ +"""Support for Modern Forms Fan Fans.""" +from __future__ import annotations + +from functools import partial +from typing import Any, Callable + +from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON +import voluptuous as vol + +from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.percentage import ( + int_states_in_range, + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from . import ( + ModernFormsDataUpdateCoordinator, + ModernFormsDeviceEntity, + modernforms_exception_handler, +) +from .const import ( + ATTR_SLEEP_TIME, + CLEAR_TIMER, + DOMAIN, + OPT_ON, + OPT_SPEED, + SERVICE_CLEAR_FAN_SLEEP_TIMER, + SERVICE_SET_FAN_SLEEP_TIMER, +) + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up a Modern Forms platform from config entry.""" + + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_FAN_SLEEP_TIMER, + { + vol.Required(ATTR_SLEEP_TIME): vol.All( + vol.Coerce(int), vol.Range(min=1, max=1440) + ), + }, + "async_set_fan_sleep_timer", + ) + + platform.async_register_entity_service( + SERVICE_CLEAR_FAN_SLEEP_TIMER, + {}, + "async_clear_fan_sleep_timer", + ) + + update_func = partial( + async_update_fan, config_entry, coordinator, {}, async_add_entities + ) + coordinator.async_add_listener(update_func) + update_func() + + +class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): + """Defines a Modern Forms light.""" + + SPEED_RANGE = (1, 6) # off is not included + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms light.""" + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=f"{coordinator.data.info.device_name} Fan", + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_fan" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_DIRECTION | SUPPORT_SET_SPEED + + @property + def percentage(self) -> int | None: + """Return the current speed percentage.""" + percentage = 0 + if bool(self.coordinator.data.state.fan_on): + percentage = ranged_value_to_percentage( + self.SPEED_RANGE, self.coordinator.data.state.fan_speed + ) + return percentage + + @property + def current_direction(self) -> str: + """Return the current direction of the fan.""" + return self.coordinator.data.state.fan_direction + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(self.SPEED_RANGE) + + @property + def is_on(self) -> bool: + """Return the state of the fan.""" + return bool(self.coordinator.data.state.fan_on) + + @modernforms_exception_handler + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + await self.coordinator.modernforms.fan(direction=direction) + + @modernforms_exception_handler + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage > 0: + await self.async_turn_on(percentage=percentage) + else: + await self.async_turn_off() + + @modernforms_exception_handler + async def async_turn_on( + self, + speed: int | None = None, + percentage: int | None = None, + preset_mode: int | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + data = {OPT_ON: FAN_POWER_ON} + + if percentage: + data[OPT_SPEED] = round( + percentage_to_ranged_value(self.SPEED_RANGE, percentage) + ) + await self.coordinator.modernforms.fan(**data) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.coordinator.modernforms.fan(on=FAN_POWER_OFF) + + @modernforms_exception_handler + async def async_set_fan_sleep_timer( + self, + sleep_time: int, + ) -> None: + """Set a Modern Forms light sleep timer.""" + await self.coordinator.modernforms.fan(sleep=sleep_time * 60) + + @modernforms_exception_handler + async def async_clear_fan_sleep_timer( + self, + ) -> None: + """Clear a Modern Forms fan sleep timer.""" + await self.coordinator.modernforms.fan(sleep=CLEAR_TIMER) + + +@callback +def async_update_fan( + entry: ConfigEntry, + coordinator: ModernFormsDataUpdateCoordinator, + current: dict[str, ModernFormsFanEntity], + async_add_entities, +) -> None: + """Update Modern Forms Fan info.""" + if not current: + current[entry.entry_id] = ModernFormsFanEntity( + entry_id=entry.entry_id, coordinator=coordinator + ) + async_add_entities([current[entry.entry_id]]) diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json new file mode 100644 index 00000000000..11d50e7353b --- /dev/null +++ b/homeassistant/components/modern_forms/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "modern_forms", + "name": "Modern Forms", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/modern_forms", + "requirements": [ + "aiomodernforms==0.1.5" + ], + "zeroconf": [ + {"type":"_easylink._tcp.local.", "name":"wac*"} + ], + "dependencies": [], + "codeowners": [ + "@wonderslug" + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml new file mode 100644 index 00000000000..6eeb423c36f --- /dev/null +++ b/homeassistant/components/modern_forms/services.yaml @@ -0,0 +1,28 @@ +set_fan_sleep_timer: + name: Set fan sleep timer + description: Set a sleep timer on a Modern Forms fan. + target: + entity: + integration: modern_forms + domain: fan + fields: + sleep_time: + name: Sleep Time + description: Number of seconds to set the timer. + required: true + example: "900" + selector: + number: + min: 1 + max: 1440 + step: 1 + unit_of_measurement: minutes + mode: slider + +clear_fan_sleep_timer: + name: Clear fan sleep timer + description: Clear the sleep timer on a Modern Forms fan. + target: + entity: + integration: modern_forms + domain: fan diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json new file mode 100644 index 00000000000..097217692ae --- /dev/null +++ b/homeassistant/components/modern_forms/strings.json @@ -0,0 +1,28 @@ +{ + "title": "Modern Forms", + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Set up your Modern Forms fan to integrate with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?", + "title": "Discovered Modern Forms fan device" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/homeassistant/components/modern_forms/translations/en.json b/homeassistant/components/modern_forms/translations/en.json new file mode 100644 index 00000000000..f25f5124ab4 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Set up your Modern Forms fan to integrate with Home Assistant." + }, + "zeroconf_confirm": { + "description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?", + "title": "Discovered Modern Forms fan device" + } + } + }, + "title": "Modern Forms" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 79245491a7e..b887574c055 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -156,6 +156,7 @@ FLOWS = [ "mill", "minecraft_server", "mobile_app", + "modern_forms", "monoprice", "motion_blinds", "motioneye", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 125ff34206a..11fd47469f8 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -60,6 +60,12 @@ ZEROCONF = { "domain": "devolo_home_control" } ], + "_easylink._tcp.local.": [ + { + "domain": "modern_forms", + "name": "wac*" + } + ], "_elg._tcp.local.": [ { "domain": "elgato" diff --git a/requirements_all.txt b/requirements_all.txt index 14b38e89d83..15f6de35a05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -205,6 +205,9 @@ aiolip==1.1.4 # homeassistant.components.lyric aiolyric==1.0.7 +# homeassistant.components.modern_forms +aiomodernforms==0.1.5 + # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8404675bea3..df69e38a5a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -130,6 +130,9 @@ aiolip==1.1.4 # homeassistant.components.lyric aiolyric==1.0.7 +# homeassistant.components.modern_forms +aiomodernforms==0.1.5 + # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/modern_forms/__init__.py b/tests/components/modern_forms/__init__.py new file mode 100644 index 00000000000..54fcf53ce89 --- /dev/null +++ b/tests/components/modern_forms/__init__.py @@ -0,0 +1,65 @@ +"""Tests for the Modern Forms integration.""" + +import json +from typing import Callable + +from aiomodernforms.const import COMMAND_QUERY_STATIC_DATA + +from homeassistant.components.modern_forms.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse + + +async def modern_forms_call_mock(method, url, data): + """Set up the basic returns based on info or status request.""" + if COMMAND_QUERY_STATIC_DATA in data: + fixture = "modern_forms/device_info.json" + else: + fixture = "modern_forms/device_status.json" + response = AiohttpClientMockResponse( + method=method, url=url, json=json.loads(load_fixture(fixture)) + ) + return response + + +async def modern_forms_no_light_call_mock(method, url, data): + """Set up the basic returns based on info or status request.""" + if COMMAND_QUERY_STATIC_DATA in data: + fixture = "modern_forms/device_info_no_light.json" + else: + fixture = "modern_forms/device_status_no_light.json" + response = AiohttpClientMockResponse( + method=method, url=url, json=json.loads(load_fixture(fixture)) + ) + return response + + +async def init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + rgbw: bool = False, + skip_setup: bool = False, + mock_type: Callable = modern_forms_call_mock, +) -> MockConfigEntry: + """Set up the Modern Forms integration in Home Assistant.""" + + aioclient_mock.post( + "http://192.168.1.123:80/mf", + side_effect=mock_type, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "AA:BB:CC:DD:EE:FF"} + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py new file mode 100644 index 00000000000..e7b01bf2fd4 --- /dev/null +++ b/tests/components/modern_forms/test_config_flow.py @@ -0,0 +1,198 @@ +"""Tests for the Modern Forms config flow.""" +from unittest.mock import MagicMock, patch + +import aiohttp +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.modern_forms.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://192.168.1.123:80/mf", + text=load_fixture("modern_forms/device_info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result.get("step_id") == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} + ) + + assert result.get("title") == "ModernFormsFan" + assert "data" in result + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_HOST] == "192.168.1.123" + assert result["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" + + +async def test_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://192.168.1.123:80/mf", + text=load_fixture("modern_forms/device_info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + assert result.get("description_placeholders") == {CONF_NAME: "example"} + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + flow = flows[0] + assert "context" in flow + assert flow["context"][CONF_HOST] == "192.168.1.123" + assert flow["context"][CONF_NAME] == "example" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2.get("title") == "example" + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + + assert "data" in result2 + assert result2["data"][CONF_HOST] == "192.168.1.123" + assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" + + +@patch( + "homeassistant.components.modern_forms.ModernFormsDevice.update", + side_effect=ModernFormsConnectionError, +) +async def test_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on Modern Forms connection error.""" + aioclient_mock.post("http://example.com/mf", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.com"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + +@patch( + "homeassistant.components.modern_forms.ModernFormsDevice.update", + side_effect=ModernFormsConnectionError, +) +async def test_zeroconf_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on Modern Forms connection error.""" + aioclient_mock.post("http://192.168.1.123/mf", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +@patch( + "homeassistant.components.modern_forms.ModernFormsDevice.update", + side_effect=ModernFormsConnectionError, +) +async def test_zeroconf_confirm_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on Modern Forms connection error.""" + aioclient_mock.post("http://192.168.1.123:80/mf", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.com", + CONF_NAME: "test", + }, + data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if Modern Forms device already configured.""" + aioclient_mock.post( + "http://192.168.1.123:80/mf", + text=load_fixture("modern_forms/device_info.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "192.168.1.123"}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_zeroconf_with_mac_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if a Modern Forms device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + }, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" diff --git a/tests/components/modern_forms/test_fan.py b/tests/components/modern_forms/test_fan.py new file mode 100644 index 00000000000..bac9a5ed07c --- /dev/null +++ b/tests/components/modern_forms/test_fan.py @@ -0,0 +1,213 @@ +"""Tests for the Modern Forms fan platform.""" +from unittest.mock import patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_PERCENTAGE, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, +) +from homeassistant.components.modern_forms.const import ( + ATTR_SLEEP_TIME, + DOMAIN, + SERVICE_CLEAR_FAN_SLEEP_TIMER, + SERVICE_SET_FAN_SLEEP_TIMER, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_fan_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms fans.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("fan.modernformsfan_fan") + assert state + assert state.attributes.get(ATTR_PERCENTAGE) == 50 + assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_FORWARD + assert state.state == STATE_ON + + entry = entity_registry.async_get("fan.modernformsfan_fan") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF_fan" + + +async def test_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms fan.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.modernformsfan_fan"}, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with( + on=False, + ) + + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "fan.modernformsfan_fan", + ATTR_PERCENTAGE: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with(on=True, speed=6) + + +async def test_sleep_timer_services( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_SLEEP_TIMER, + {ATTR_ENTITY_ID: "fan.modernformsfan_fan", ATTR_SLEEP_TIME: 1}, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with(sleep=60) + + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_FAN_SLEEP_TIMER, + {ATTR_ENTITY_ID: "fan.modernformsfan_fan"}, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with(sleep=0) + + +async def test_change_direction( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_DIRECTION, + { + ATTR_ENTITY_ID: "fan.modernformsfan_fan", + ATTR_DIRECTION: DIRECTION_REVERSE, + }, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with( + direction=DIRECTION_REVERSE, + ) + + +async def test_set_percentage( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of percentage for the Modern Forms fan.""" + await init_integration(hass, aioclient_mock) + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + { + ATTR_ENTITY_ID: "fan.modernformsfan_fan", + ATTR_PERCENTAGE: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with( + on=True, + speed=6, + ) + + await init_integration(hass, aioclient_mock) + with patch("aiomodernforms.ModernFormsDevice.fan") as fan_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + { + ATTR_ENTITY_ID: "fan.modernformsfan_fan", + ATTR_PERCENTAGE: 0, + }, + blocking=True, + ) + await hass.async_block_till_done() + fan_mock.assert_called_once_with(on=False) + + +async def test_fan_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the Modern Forms fans.""" + + await init_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() + + aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.modernformsfan_fan"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("fan.modernformsfan_fan") + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + + +async def test_fan_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the Moder Forms fans.""" + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( + "homeassistant.components.modern_forms.ModernFormsDevice.fan", + side_effect=ModernFormsConnectionError, + ): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.modernformsfan_fan"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("fan.modernformsfan_fan") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/modern_forms/test_init.py b/tests/components/modern_forms/test_init.py new file mode 100644 index 00000000000..6ef7b563918 --- /dev/null +++ b/tests/components/modern_forms/test_init.py @@ -0,0 +1,60 @@ +"""Tests for the Modern Forms integration.""" +from unittest.mock import MagicMock, patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.modern_forms.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import ( + init_integration, + modern_forms_no_light_call_mock, +) +from tests.test_util.aiohttp import AiohttpClientMocker + + +@patch( + "homeassistant.components.modern_forms.ModernFormsDevice.update", + side_effect=ModernFormsConnectionError, +) +async def test_config_entry_not_ready( + mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Modern Forms configuration entry not ready.""" + entry = await init_integration(hass, aioclient_mock) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Modern Forms configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) + + +async def test_setting_unique_id(hass, aioclient_mock): + """Test we set unique ID if not set yet.""" + entry = await init_integration(hass, aioclient_mock) + + assert hass.data[DOMAIN] + assert entry.unique_id == "AA:BB:CC:DD:EE:FF" + + +async def test_fan_only_device(hass, aioclient_mock): + """Test we set unique ID if not set yet.""" + await init_integration( + hass, aioclient_mock, mock_type=modern_forms_no_light_call_mock + ) + entity_registry = er.async_get(hass) + + fan_entry = entity_registry.async_get("fan.modernformsfan_fan") + assert fan_entry + light_entry = entity_registry.async_get("light.modernformsfan_light") + assert light_entry is None diff --git a/tests/fixtures/modern_forms/device_info.json b/tests/fixtures/modern_forms/device_info.json new file mode 100644 index 00000000000..e63f79fd468 --- /dev/null +++ b/tests/fixtures/modern_forms/device_info.json @@ -0,0 +1,15 @@ +{ + "clientId": "MF_000000000000", + "mac": "AA:BB:CC:DD:EE:FF", + "lightType": "F6IN-120V-R1-30", + "fanType": "1818-56", + "fanMotorType": "DC125X25", + "productionLotNumber": "", + "productSku": "", + "owner": "someone@somewhere.com", + "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", + "deviceName": "ModernFormsFan", + "firmwareVersion": "01.03.0025", + "mainMcuFirmwareVersion": "01.03.3008", + "firmwareUrl": "" +} diff --git a/tests/fixtures/modern_forms/device_info_no_light.json b/tests/fixtures/modern_forms/device_info_no_light.json new file mode 100644 index 00000000000..5557af57531 --- /dev/null +++ b/tests/fixtures/modern_forms/device_info_no_light.json @@ -0,0 +1,14 @@ +{ + "clientId": "MF_000000000000", + "mac": "AA:BB:CC:DD:EE:FF", + "fanType": "1818-56", + "fanMotorType": "DC125X25", + "productionLotNumber": "", + "productSku": "", + "owner": "someone@somewhere.com", + "federatedIdentity": "us-east-1:f3da237b-c19c-4f61-b387-0e6dde2e470b", + "deviceName": "ModernFormsFan", + "firmwareVersion": "01.03.0025", + "mainMcuFirmwareVersion": "01.03.3008", + "firmwareUrl": "" +} diff --git a/tests/fixtures/modern_forms/device_status.json b/tests/fixtures/modern_forms/device_status.json new file mode 100644 index 00000000000..c982f884375 --- /dev/null +++ b/tests/fixtures/modern_forms/device_status.json @@ -0,0 +1,17 @@ +{ + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 0, + "fanSpeed": 3, + "lightBrightness": 50, + "lightOn": true, + "lightSleepTimer": 0, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" +} diff --git a/tests/fixtures/modern_forms/device_status_no_light.json b/tests/fixtures/modern_forms/device_status_no_light.json new file mode 100644 index 00000000000..ca499b271fb --- /dev/null +++ b/tests/fixtures/modern_forms/device_status_no_light.json @@ -0,0 +1,14 @@ +{ + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 0, + "fanSpeed": 3, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" +} From 4e5ec26ce6aa2b8056c1fa7b47b432a0d3096c52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Jun 2021 08:23:35 +0200 Subject: [PATCH 238/750] Remove value_template from MQTT_RW_PLATFORM_SCHEMA (#51590) --- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/mqtt/light/schema_basic.py | 1 + homeassistant/components/mqtt/lock.py | 1 + homeassistant/components/mqtt/switch.py | 1 + 4 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index de7fb69b8b6..3a6fd068975 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -220,7 +220,6 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 684dcf337aa..b1f3dc7c59b 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -166,6 +166,7 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 24d58b148fa..a48d271c196 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -45,6 +45,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index d07f639f41d..5dfc26ce613 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -47,6 +47,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_STATE_ON): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) From a4587b5f3b31ddf74b562f7a26f992b3b5dfd33f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Jun 2021 08:23:51 +0200 Subject: [PATCH 239/750] Deprecate support for undocumented value_template in MQTT light (#51589) --- homeassistant/components/mqtt/light/schema_basic.py | 6 ++++-- tests/components/mqtt/test_light.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index b1f3dc7c59b..2030cfb8825 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -122,7 +122,9 @@ VALUE_TEMPLATE_KEYS = [ CONF_XY_VALUE_TEMPLATE, ] -PLATFORM_SCHEMA_BASIC = ( +PLATFORM_SCHEMA_BASIC = vol.All( + # CONF_VALUE_TEMPLATE is deprecated, support will be removed in 2021.10 + cv.deprecated(CONF_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE), mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -179,7 +181,7 @@ PLATFORM_SCHEMA_BASIC = ( } ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) - .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) + .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema), ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e419743bd87..728656743e5 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1103,7 +1103,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): +async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock, caplog): """Test the setting of the state with undocumented value_template.""" config = { light.DOMAIN: { @@ -1118,6 +1118,8 @@ async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + assert "The 'value_template' option is deprecated" in caplog.text + state = hass.states.get("light.test") assert state.state == STATE_OFF From 4007430d7262ef035bb80affea13657fdc993b1d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Jun 2021 08:29:04 +0200 Subject: [PATCH 240/750] Small entity attribute cleanup in AirVisual (#51601) * Small entity attribute cleanup in AirVisual * Fix icon in sensor update --- homeassistant/components/airvisual/__init__.py | 12 ------------ homeassistant/components/airvisual/air_quality.py | 4 +--- homeassistant/components/airvisual/sensor.py | 15 ++++++--------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index ac34c16d3d0..8a1e0ad9655 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -337,24 +337,12 @@ class AirVisualEntity(CoordinatorEntity): """Initialize.""" super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = None - self._unit = None @property def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py index 047367fa67c..175c129068f 100644 --- a/homeassistant/components/airvisual/air_quality.py +++ b/homeassistant/components/airvisual/air_quality.py @@ -1,6 +1,5 @@ """Support for AirVisual Node/Pro units.""" from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from homeassistant.core import callback from . import AirVisualEntity @@ -34,8 +33,7 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): """Initialize.""" super().__init__(airvisual) - self._icon = "mdi:chemical-weapon" - self._unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + self._attr_icon = "mdi:chemical-weapon" @property def air_quality_index(self): diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 1febcec68f4..ec8fe108b7f 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -154,12 +154,13 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): } ) self._config_entry = config_entry - self._icon = icon self._kind = kind self._locale = locale self._name = name self._state = None - self._unit = unit + + self._attr_icon = icon + self._attr_unit_of_measurement = unit @property def available(self): @@ -196,7 +197,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - self._state, self._icon = async_get_pollutant_level_info(aqi) + self._state, self._attr_icon = async_get_pollutant_level_info(aqi) elif self._kind == SENSOR_KIND_AQI: self._state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: @@ -244,16 +245,12 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator) - self._device_class = device_class self._kind = kind self._name = name self._state = None - self._unit = unit - @property - def device_class(self): - """Return the device class.""" - return self._device_class + self._attr_device_class = device_class + self._attr_unit_of_measurement = unit @property def device_info(self): From 502939c43045560c6c40ff63df3eae0eb65642d5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Jun 2021 13:23:25 +0200 Subject: [PATCH 241/750] Do not configure Shelly config entry created by custom component (#51616) --- homeassistant/components/shelly/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4d7b8654720..425ff11399b 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,6 +68,18 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly from a config entry.""" + # The custom component for Shelly devices uses shelly domain as well as core + # integration. If the user removes the custom component but doesn't remove the + # config entry, core integration will try to configure that config entry with an + # error. The config entry data for this custom component doesn't contain host + # value, so if host isn't present, config entry will not be configured. + if not entry.data.get(CONF_HOST): + _LOGGER.warning( + "The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration", + entry.title, + ) + return False + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None From fa42c676bb095d0dcbc254abc37b4ad1acbe429f Mon Sep 17 00:00:00 2001 From: blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Tue, 8 Jun 2021 13:20:15 +0100 Subject: [PATCH 242/750] Reduce ovo_energy polling rate to be less aggressive (#51613) * Reduce polling rate to be less aggressive The current polling rate is too aggressive for the purpose, this commit reduces it to 12 hours to play nice with OVO. * tweak polling to hourly --- homeassistant/components/ovo_energy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 18414db7292..79a8e6138eb 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="sensor", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), + update_interval=timedelta(seconds=3600), ) hass.data.setdefault(DOMAIN, {}) From 3fa6c97801c2d2985490166d84c600e27db7a33a Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Tue, 8 Jun 2021 22:26:43 +1000 Subject: [PATCH 243/750] Address late review of nsw fuel station (#51619) --- homeassistant/components/nsw_fuel_station/__init__.py | 7 ++++--- tests/components/nsw_fuel_station/test_sensor.py | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nsw_fuel_station/__init__.py b/homeassistant/components/nsw_fuel_station/__init__.py index a79f164ba0f..f64365461f4 100644 --- a/homeassistant/components/nsw_fuel_station/__init__.py +++ b/homeassistant/components/nsw_fuel_station/__init__.py @@ -7,7 +7,7 @@ import logging from nsw_fuel import FuelCheckClient, FuelCheckError, Station -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_NSW_FUEL_STATION @@ -60,5 +60,6 @@ def fetch_station_price_data(client: FuelCheckClient) -> StationPriceData | None ) except FuelCheckError as exc: - _LOGGER.error("Failed to fetch NSW Fuel station price data. %s", exc) - return None + raise UpdateFailed( + f"Failed to fetch NSW Fuel station price data: {exc}" + ) from exc diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index a9704256655..c348c7adb9c 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -75,7 +75,6 @@ MOCK_FUEL_PRICES_RESPONSE = MockGetFuelPricesResponse( ) async def test_setup(get_fuel_prices, hass): """Test the setup with custom settings.""" - assert await async_setup_component(hass, DOMAIN, {}) with assert_setup_component(1, sensor.DOMAIN): assert await async_setup_component( hass, sensor.DOMAIN, {"sensor": VALID_CONFIG} @@ -98,7 +97,6 @@ def raise_fuel_check_error(): ) async def test_setup_error(get_fuel_prices, hass): """Test the setup with client throwing error.""" - assert await async_setup_component(hass, DOMAIN, {}) with assert_setup_component(1, sensor.DOMAIN): assert await async_setup_component( hass, sensor.DOMAIN, {"sensor": VALID_CONFIG} @@ -116,7 +114,6 @@ async def test_setup_error(get_fuel_prices, hass): ) async def test_setup_error_no_station(get_fuel_prices, hass): """Test the setup with specified station not existing.""" - assert await async_setup_component(hass, DOMAIN, {}) with assert_setup_component(2, sensor.DOMAIN): assert await async_setup_component( hass, From 6de604a326ded8945cd080645720780619de155a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Jun 2021 14:28:36 +0200 Subject: [PATCH 244/750] Fix mysensors tests typing (#51621) --- .../components/mysensors/test_config_flow.py | 26 +++++++++++-------- tests/components/mysensors/test_gateway.py | 4 ++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index 161d00e44b3..ddefd55457f 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -25,13 +25,14 @@ from homeassistant.components.mysensors.const import ( ConfGatewayType, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from tests.common import MockConfigEntry async def get_form( hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str -): +) -> FlowResult: """Get a form for the given gateway type.""" await setup.async_setup_component(hass, "persistent_notification", {}) stepuser = await hass.config_entries.flow.async_init( @@ -107,7 +108,7 @@ async def test_missing_mqtt(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "mqtt_required"} -async def test_config_serial(hass: HomeAssistant): +async def test_config_serial(hass: HomeAssistant) -> None: """Test configuring a gateway via serial.""" step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial") flow_id = step["flow_id"] @@ -147,7 +148,7 @@ async def test_config_serial(hass: HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 -async def test_config_tcp(hass: HomeAssistant): +async def test_config_tcp(hass: HomeAssistant) -> None: """Test configuring a gateway via tcp.""" step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] @@ -184,7 +185,7 @@ async def test_config_tcp(hass: HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 -async def test_fail_to_connect(hass: HomeAssistant): +async def test_fail_to_connect(hass: HomeAssistant) -> None: """Test configuring a gateway via tcp.""" step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] @@ -209,8 +210,9 @@ async def test_fail_to_connect(hass: HomeAssistant): assert result2["type"] == "form" assert "errors" in result2 - assert "base" in result2["errors"] - assert result2["errors"]["base"] == "cannot_connect" + errors = result2["errors"] + assert errors + assert errors.get("base") == "cannot_connect" assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -367,12 +369,12 @@ async def test_fail_to_connect(hass: HomeAssistant): ) async def test_config_invalid( hass: HomeAssistant, - mqtt: config_entries.ConfigEntry, + mqtt: None, gateway_type: ConfGatewayType, expected_step_id: str, user_input: dict[str, Any], - err_field, - err_string, + err_field: str, + err_string: str, ) -> None: """Perform a test that is expected to generate an error.""" step = await get_form(hass, gateway_type, expected_step_id) @@ -397,8 +399,10 @@ async def test_config_invalid( assert result2["type"] == "form" assert "errors" in result2 - assert err_field in result2["errors"] - assert result2["errors"][err_field] == err_string + errors = result2["errors"] + assert errors + assert err_field in errors + assert errors[err_field] == err_string assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py index f2e7aa77c8c..0c9652bdfc1 100644 --- a/tests/components/mysensors/test_gateway.py +++ b/tests/components/mysensors/test_gateway.py @@ -18,7 +18,9 @@ from homeassistant.core import HomeAssistant ("/dev/ttyACM0", False), ], ) -def test_is_serial_port_windows(hass: HomeAssistant, port: str, expect_valid: bool): +def test_is_serial_port_windows( + hass: HomeAssistant, port: str, expect_valid: bool +) -> None: """Test windows serial port.""" with patch("sys.platform", "win32"): From 7790e8f90cab16c43d4129f55e9ad8202e2168cc Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Tue, 8 Jun 2021 17:03:28 +0300 Subject: [PATCH 245/750] Static typing for Zodiac (#51622) --- .strict-typing | 1 + homeassistant/components/zodiac/__init__.py | 3 +- homeassistant/components/zodiac/sensor.py | 34 +++++++++++++-------- mypy.ini | 11 +++++++ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/.strict-typing b/.strict-typing index 23b4ed76513..2ad7d0d1107 100644 --- a/.strict-typing +++ b/.strict-typing @@ -74,6 +74,7 @@ homeassistant.components.vacuum.* homeassistant.components.water_heater.* homeassistant.components.weather.* homeassistant.components.websocket_api.* +homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* homeassistant.components.zwave_js.* diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index d00cc560f22..c19b7a45ac2 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -12,7 +13,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index 4c037a7aa02..80a4f782915 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -1,5 +1,10 @@ """Support for tracking the zodiac sign.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import as_local, utcnow from .const import ( @@ -154,7 +159,12 @@ ZODIAC_ICONS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the Zodiac sensor platform.""" if discovery_info is None: return @@ -165,42 +175,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ZodiacSensor(SensorEntity): """Representation of a Zodiac sensor.""" - def __init__(self): + def __init__(self) -> None: """Initialize the zodiac sensor.""" - self._attrs = None - self._state = None + self._attrs: dict[str, str] = {} + self._state: str = "" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return DOMAIN @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return "Zodiac" @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the entity.""" return "zodiac__sign" @property - def state(self): + def state(self) -> str: """Return the state of the device.""" return self._state @property - def icon(self): - """Icon to use in the frontend, if any.""" + def icon(self) -> str | None: + """Icon to use in the frontend.""" return ZODIAC_ICONS.get(self._state) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return self._attrs - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the state.""" today = as_local(utcnow()).date() diff --git a/mypy.ini b/mypy.ini index bada042219a..c25dc4ec1f6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -825,6 +825,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.zodiac.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zeroconf.*] check_untyped_defs = true disallow_incomplete_defs = true From b3a67a2dd7f829f8389bae4b88ab98d6fa59b652 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Jun 2021 04:53:51 -1000 Subject: [PATCH 246/750] Bump sqlalchemy to 1.4.17 (#51593) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 6a3f6ae6b54..7e4c9d9b9fa 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.13"], + "requirements": ["sqlalchemy==1.4.17"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index e9805040648..a2a197a0eb0 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.13"], + "requirements": ["sqlalchemy==1.4.17"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8705b8551f8..85a06e43413 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.5 -sqlalchemy==1.4.13 +sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 15f6de35a05..820545466c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2156,7 +2156,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.13 +sqlalchemy==1.4.17 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df69e38a5a6..7c3d5c82c31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1170,7 +1170,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.13 +sqlalchemy==1.4.17 # homeassistant.components.srp_energy srpenergy==1.3.2 From 67f3e717a8b5e3da5954da70e6fe91c0919ab876 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Jun 2021 17:43:04 +0200 Subject: [PATCH 247/750] Add support for color_mode white to tasmota light (#51608) --- homeassistant/components/tasmota/light.py | 65 ++++++---------- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/light/common.py | 5 ++ tests/components/tasmota/test_light.py | 75 ++++++++++++------- 6 files changed, 82 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 58a1ff1fb23..edbb01eefe6 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -13,13 +13,13 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, - COLOR_MODE_RGBW, + COLOR_MODE_WHITE, SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, @@ -60,6 +60,17 @@ def clamp(value): return min(max(value, 0), 255) +def scale_brightness(brightness): + """Scale brightness from 0..255 to 1..100.""" + brightness_normalized = brightness / DEFAULT_BRIGHTNESS_MAX + device_brightness = min( + round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), + TASMOTA_BRIGHTNESS_MAX, + ) + # Make sure the brightness is not rounded down to 0 + return max(device_brightness, 1) + + class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, @@ -80,7 +91,6 @@ class TasmotaLight( self._white_value = None self._flash_times = None self._rgb = None - self._rgbw = None super().__init__( **kwds, @@ -107,8 +117,7 @@ class TasmotaLight( self._color_mode = COLOR_MODE_RGB if light_type == LIGHT_TYPE_RGBW: - self._supported_color_modes.add(COLOR_MODE_RGBW) - self._color_mode = COLOR_MODE_RGBW + self._supported_color_modes.add(COLOR_MODE_WHITE) if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]: self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -150,7 +159,13 @@ class TasmotaLight( white_value = float(attributes["white_value"]) percent_white = white_value / TASMOTA_BRIGHTNESS_MAX self._white_value = percent_white * 255 - if self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: + if self._tasmota_entity.light_type == LIGHT_TYPE_RGBW: + # Tasmota does not support RGBW mode, set mode to white or rgb + if self._white_value == 0: + self._color_mode = COLOR_MODE_RGB + else: + self._color_mode = COLOR_MODE_WHITE + elif self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: # Tasmota does not support RGBWW mode, set mode to ct or rgb if self._white_value == 0: self._color_mode = COLOR_MODE_RGB @@ -211,30 +226,9 @@ class TasmotaLight( blue_compensated = 0 return [red_compensated, green_compensated, blue_compensated] - @property - def rgbw_color(self): - """Return the rgbw color value.""" - if self._rgb is None or self._white_value is None: - return None - rgb = self._rgb - # Tasmota's color is adjusted for brightness, compensate - if self._brightness > 0: - red_compensated = clamp(round(rgb[0] / self._brightness * 255)) - green_compensated = clamp(round(rgb[1] / self._brightness * 255)) - blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) - white_compensated = clamp(round(self._white_value / self._brightness * 255)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - white_compensated = 0 - return [red_compensated, green_compensated, blue_compensated, white_compensated] - @property def force_update(self): """Force update.""" - if self.color_mode == COLOR_MODE_RGBW: - return True return False @property @@ -262,25 +256,14 @@ class TasmotaLight( rgb = kwargs[ATTR_RGB_COLOR] attributes["color"] = [rgb[0], rgb[1], rgb[2]] - if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: - rgbw = kwargs[ATTR_RGBW_COLOR] - attributes["color"] = [rgbw[0], rgbw[1], rgbw[2], rgbw[3]] - # Tasmota does not support direct RGBW control, the light must be set to - # either white mode or color mode. Set the mode to white if white channel - # is on, and to color otherwise + if ATTR_WHITE in kwargs and COLOR_MODE_WHITE in supported_color_modes: + attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE]) if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): - brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_MAX - device_brightness = min( - round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), - TASMOTA_BRIGHTNESS_MAX, - ) - # Make sure the brightness is not rounded down to 0 - device_brightness = max(device_brightness, 1) - attributes["brightness"] = device_brightness + attributes["brightness"] = scale_brightness(kwargs[ATTR_BRIGHTNESS]) if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index cb869b6099c..1c73e99e916 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.14"], + "requirements": ["hatasmota==0.2.15"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 820545466c0..87ca3323c73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.14 +hatasmota==0.2.15 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c3d5c82c31..75aa56d77ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ hangups==0.4.14 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.14 +hatasmota==0.2.15 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 229823ceb17..0c16e0f2703 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -17,6 +17,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, @@ -50,6 +51,7 @@ def turn_on( flash=None, effect=None, color_name=None, + white=None, ): """Turn all or specified light on.""" hass.add_job( @@ -71,6 +73,7 @@ def turn_on( flash, effect, color_name, + white, ) @@ -92,6 +95,7 @@ async def async_turn_on( flash=None, effect=None, color_name=None, + white=None, ): """Turn all or specified light on.""" data = { @@ -113,6 +117,7 @@ async def async_turn_on( (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), + (ATTR_WHITE, white), ] if value is not None } diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index b74799d1d12..0c0e0a4e566 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -223,8 +223,8 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["rgb", "rgbw"] - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("supported_color_modes") == ["rgb", "white"] + assert state.attributes.get("color_mode") == "rgb" async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): @@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") @@ -442,24 +442,31 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): assert "color_mode" not in state.attributes async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 191.25 + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Color":"128,64,0","White":0}', + '{"POWER":"ON","Dimmer":50,"Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("rgbw_color") == (255, 128, 0, 0) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' @@ -467,9 +474,8 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("rgb_color") == (255, 192, 128) - assert state.attributes.get("rgbw_color") == (255, 128, 0, 255) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' @@ -477,9 +483,8 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 0 - assert state.attributes.get("rgb_color") == (0, 0, 0) - assert state.attributes.get("rgbw_color") == (0, 0, 0, 0) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -959,21 +964,31 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - # Set color when setting white is off - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + # Set white when setting white + await common.async_turn_on(hass, "light.test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + "NoDelay;Power1 ON;NoDelay;White 50", 0, False, ) mqtt_mock.async_publish.reset_mock() - # Set white when white is on + # rgbw_color should be ignored + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # rgbw_color should be ignored await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;White 50", + "NoDelay;Power1 ON", 0, False, ) @@ -1041,7 +1056,7 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): # Turn the light on and verify MQTT messages are sent await common.async_turn_on(hass, "light.test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer4 75", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -1055,21 +1070,31 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # Set color when setting white is off - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + # Set white when setting white + await common.async_turn_on(hass, "light.test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32,0", + "NoDelay;Power1 ON;NoDelay;White 50", 0, False, ) mqtt_mock.async_publish.reset_mock() - # Set white when white is on + # rgbw_color should be ignored + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # rgbw_color should be ignored await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 16,64,32,128", + "NoDelay;Power1 ON", 0, False, ) From 79da2bca3f210571f17a5eed64de861447392bda Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Jun 2021 18:12:49 +0200 Subject: [PATCH 248/750] Use baseimage 2021.06.0 / Python 3.9 - Alpine 3.13 (#51628) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index c3e5d83dc78..e3d614a8511 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.05.0", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.05.0", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.05.0", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.05.0", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.05.0" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.0" }, "labels": { "io.hass.type": "core", From abbd4d1d16f00e228a702fdef24e28acb33db4af Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 8 Jun 2021 20:01:36 +0200 Subject: [PATCH 249/750] Fix Onvif get_time_zone from device (#51620) Co-authored-by: Martin Hjelmare --- homeassistant/components/onvif/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 35dc436d201..87b68508fa1 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -169,7 +169,9 @@ class ONVIFDevice: cdate = device_time.UTCDateTime else: tzone = ( - dt_util.get_time_zone(device_time.TimeZone) + dt_util.get_time_zone( + device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE) + ) or dt_util.DEFAULT_TIME_ZONE ) cdate = device_time.LocalDateTime From 2eb6f16a94a4e15a7b0087b3cc9a649bd2540613 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Jun 2021 20:24:54 +0200 Subject: [PATCH 250/750] Fix mysensors awesomeversion strategy usage (#51627) * Update awesomeversion strategy use in mysensors * Remove default version --- .../components/mysensors/config_flow.py | 16 +++++++--------- tests/components/mysensors/test_config_flow.py | 12 ++---------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 1abf45dd60f..920cb40b7ab 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -1,7 +1,6 @@ """Config flow for MySensors.""" from __future__ import annotations -from contextlib import suppress import logging import os from typing import Any @@ -55,7 +54,6 @@ def _get_schema_common(user_input: dict[str, str]) -> dict: schema = { vol.Required( CONF_VERSION, - default="", description={ "suggested_value": user_input.get(CONF_VERSION, DEFAULT_VERSION) }, @@ -67,14 +65,14 @@ def _get_schema_common(user_input: dict[str, str]) -> dict: def _validate_version(version: str) -> dict[str, str]: """Validate a version string from the user.""" - version_okay = False - with suppress(AwesomeVersionStrategyException): - version_okay = bool( - AwesomeVersion.ensure_strategy( - version, - [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER], - ) + version_okay = True + try: + AwesomeVersion( + version, + [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER], ) + except AwesomeVersionStrategyException: + version_okay = False if version_okay: return {} diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index ddefd55457f..daee8a37eba 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -264,16 +264,6 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: CONF_VERSION, "invalid_version", ), - ( - CONF_GATEWAY_TYPE_TCP, - "gw_tcp", - { - CONF_TCP_PORT: 5003, - CONF_DEVICE: "127.0.0.1", - }, - CONF_VERSION, - "invalid_version", - ), ( CONF_GATEWAY_TYPE_TCP, "gw_tcp", @@ -302,6 +292,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: { CONF_TCP_PORT: 5003, CONF_DEVICE: "127.0.0.", + CONF_VERSION: "2.4", }, CONF_DEVICE, "invalid_ip", @@ -312,6 +303,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: { CONF_TCP_PORT: 5003, CONF_DEVICE: "abcd", + CONF_VERSION: "2.4", }, CONF_DEVICE, "invalid_ip", From 29a020886edd04503833c27ae7d3a275e5025896 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Jun 2021 20:48:49 +0200 Subject: [PATCH 251/750] Update Machine support of python 3.9 / Kernel CEC (#51637) --- build.json | 10 +++++----- machine/generic-x86-64 | 28 ---------------------------- machine/intel-nuc | 28 ---------------------------- machine/odroid-c2 | 29 ----------------------------- machine/odroid-c4 | 29 ----------------------------- machine/odroid-n2 | 29 ----------------------------- machine/odroid-xu | 29 ----------------------------- machine/qemuarm | 28 ---------------------------- machine/qemuarm-64 | 28 ---------------------------- machine/qemux86 | 28 ---------------------------- machine/qemux86-64 | 28 ---------------------------- machine/raspberrypi | 29 ----------------------------- machine/raspberrypi2 | 29 ----------------------------- machine/raspberrypi3 | 29 ----------------------------- machine/raspberrypi3-64 | 29 ----------------------------- machine/raspberrypi4 | 29 ----------------------------- machine/raspberrypi4-64 | 29 ----------------------------- machine/tinker | 28 ---------------------------- 18 files changed, 5 insertions(+), 491 deletions(-) diff --git a/build.json b/build.json index e3d614a8511..d2370b14773 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.0", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.0", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.0", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.0", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.0" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.1", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.1", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.1", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.1", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.1" }, "labels": { "io.hass.type": "core", diff --git a/machine/generic-x86-64 b/machine/generic-x86-64 index 4c83228387d..b7fbac2e8ed 100644 --- a/machine/generic-x86-64 +++ b/machine/generic-x86-64 @@ -4,31 +4,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ libva-intel-driver \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/intel-nuc b/machine/intel-nuc index b5538b8ccad..5e1b7f957d1 100644 --- a/machine/intel-nuc +++ b/machine/intel-nuc @@ -7,31 +7,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ libva-intel-driver \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/odroid-c2 b/machine/odroid-c2 index 9bfbb931ed0..be07d6c8aba 100644 --- a/machine/odroid-c2 +++ b/machine/odroid-c2 @@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - -DHAVE_AOCEC_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/odroid-c4 b/machine/odroid-c4 index 9bfbb931ed0..be07d6c8aba 100644 --- a/machine/odroid-c4 +++ b/machine/odroid-c4 @@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - -DHAVE_AOCEC_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/odroid-n2 b/machine/odroid-n2 index 9bfbb931ed0..be07d6c8aba 100644 --- a/machine/odroid-n2 +++ b/machine/odroid-n2 @@ -3,32 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - -DHAVE_AOCEC_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/odroid-xu b/machine/odroid-xu index 1947115f672..3aa428d3f52 100644 --- a/machine/odroid-xu +++ b/machine/odroid-xu @@ -3,32 +3,3 @@ FROM homeassistant/armv7-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - -DHAVE_EXYNOS_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/qemuarm b/machine/qemuarm index 2735a7bae23..e00c945e945 100644 --- a/machine/qemuarm +++ b/machine/qemuarm @@ -3,31 +3,3 @@ FROM homeassistant/armhf-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/qemuarm-64 b/machine/qemuarm-64 index 5783de82f58..be07d6c8aba 100644 --- a/machine/qemuarm-64 +++ b/machine/qemuarm-64 @@ -3,31 +3,3 @@ FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/qemux86 b/machine/qemux86 index 192d287dfde..1b5350df4c8 100644 --- a/machine/qemux86 +++ b/machine/qemux86 @@ -3,31 +3,3 @@ FROM homeassistant/i386-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/qemux86-64 b/machine/qemux86-64 index 5f4ca461ae8..541e994b967 100644 --- a/machine/qemux86-64 +++ b/machine/qemux86-64 @@ -3,31 +3,3 @@ FROM homeassistant/amd64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ usbutils - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* diff --git a/machine/raspberrypi b/machine/raspberrypi index c9271aceccb..3f000b14db7 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index d6c01b4ae02..484b209b6fa 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 4509e150584..1aec7ebf39f 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 97064a2377d..165dc2e5397 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 4509e150584..1aec7ebf39f 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 97064a2377d..165dc2e5397 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -15,32 +15,3 @@ RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv - -## -# Build libcec with RPi support for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - raspberrypi-dev \ - p8-platform-dev \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DRPI_INCLUDE_DIR=/opt/vc/include \ - -DRPI_LIB_DIR=/opt/vc/lib \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec -ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH} diff --git a/machine/tinker b/machine/tinker index 46b627c2257..20efbe964e8 100644 --- a/machine/tinker +++ b/machine/tinker @@ -18,31 +18,3 @@ RUN apk add --no-cache --virtual .build-dependencies \ && python3 setup.py install \ && apk del .build-dependencies \ && rm -rf /usr/src/gpio - -## -# Build libcec for HDMI-CEC -ARG LIBCEC_VERSION=6.0.2 -RUN apk add --no-cache \ - eudev-libs \ - p8-platform \ - && apk add --no-cache --virtual .build-dependencies \ - build-base \ - cmake \ - eudev-dev \ - swig \ - p8-platform-dev \ - linux-headers \ - && git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \ - && cd /usr/src/libcec \ - && mkdir -p /usr/src/libcec/build \ - && cd /usr/src/libcec/build \ - && cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \ - -DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \ - -DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \ - -DHAVE_LINUX_API=1 \ - .. \ - && make -j$(nproc) \ - && make install \ - && echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \ - && apk del .build-dependencies \ - && rm -rf /usr/src/libcec* From eb687b733233c8aa97b7963b9b522808ed65d711 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Jun 2021 20:49:13 +0200 Subject: [PATCH 252/750] Bump hatasmota to 0.2.16 (#51623) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 1c73e99e916..13248cba47d 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.15"], + "requirements": ["hatasmota==0.2.16"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 87ca3323c73..509c4133b82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.15 +hatasmota==0.2.16 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75aa56d77ce..3ec4558d80d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ hangups==0.4.14 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.15 +hatasmota==0.2.16 # homeassistant.components.jewish_calendar hdate==0.10.2 From d56bd61b9365b7e7bc4c6796d95f5a715f8ce36d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Jun 2021 10:32:06 -1000 Subject: [PATCH 253/750] Populate upnp devices from ssdp (#51221) * Populate upnp devices from ssdp * Update tests since data comes in via HASS format now * pylint --- homeassistant/components/upnp/__init__.py | 23 ++--- homeassistant/components/upnp/config_flow.py | 12 +-- homeassistant/components/upnp/device.py | 26 +++--- homeassistant/components/upnp/manifest.json | 1 + tests/components/upnp/test_config_flow.py | 77 +++++++++------- tests/components/upnp/test_init.py | 95 +++----------------- 6 files changed, 81 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 24be3b119dc..5788ec1b3ef 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -5,6 +5,7 @@ from ipaddress import ip_address import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -17,9 +18,6 @@ from .const import ( CONFIG_ENTRY_HOSTNAME, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, - DISCOVERY_LOCATION, - DISCOVERY_ST, - DISCOVERY_UDN, DOMAIN, DOMAIN_CONFIG, DOMAIN_DEVICES, @@ -49,24 +47,15 @@ async def async_construct_device(hass: HomeAssistant, udn: str, st: str) -> Devi """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name _LOGGER.debug("Constructing device: %s::%s", udn, st) + discovery_info = ssdp.async_get_discovery_info_by_udn_st(hass, udn, st) - discoveries = [ - discovery - for discovery in await Device.async_discover(hass) - if discovery[DISCOVERY_UDN] == udn and discovery[DISCOVERY_ST] == st - ] - if not discoveries: + if not discovery_info: _LOGGER.info("Device not discovered") return None - # Some additional clues for remote debugging. - if len(discoveries) > 1: - _LOGGER.info("Multiple devices discovered: %s", discoveries) - - discovery = discoveries[0] - _LOGGER.debug("Constructing from discovery: %s", discovery) - location = discovery[DISCOVERY_LOCATION] - return await Device.async_create_device(hass, location) + return await Device.async_create_device( + hass, discovery_info[ssdp.ATTR_SSDP_LOCATION] + ) async def async_setup(hass: HomeAssistant, config: ConfigType): diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 8e2dfc1f43f..f52ce89660d 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -29,17 +29,7 @@ from .const import ( DOMAIN_DEVICES, LOGGER as _LOGGER, ) -from .device import Device - - -def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: - """Convert a SSDP-discovery to 'our' discovery.""" - return { - DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], - DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], - DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], - DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], - } +from .device import Device, discovery_info_to_discovery class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 496293926d3..9af7cf55c24 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -12,6 +12,7 @@ from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.device_updater import DeviceUpdater from async_upnp_client.profiles.igd import IgdDevice +from homeassistant.components import ssdp from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -37,6 +38,16 @@ from .const import ( ) +def discovery_info_to_discovery(discovery_info: Mapping) -> Mapping: + """Convert a SSDP-discovery to 'our' discovery.""" + return { + DISCOVERY_UDN: discovery_info[ssdp.ATTR_UPNP_UDN], + DISCOVERY_ST: discovery_info[ssdp.ATTR_SSDP_ST], + DISCOVERY_LOCATION: discovery_info[ssdp.ATTR_SSDP_LOCATION], + DISCOVERY_USN: discovery_info[ssdp.ATTR_SSDP_USN], + } + + def _get_local_ip(hass: HomeAssistant) -> IPv4Address | None: """Get the configured local ip.""" if DOMAIN in hass.data and DOMAIN_CONFIG in hass.data[DOMAIN]: @@ -59,17 +70,10 @@ class Device: async def async_discover(cls, hass: HomeAssistant) -> list[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") - local_ip = _get_local_ip(hass) - discoveries = await IgdDevice.async_search(source_ip=local_ip, timeout=10) - - # Supplement/standardize discovery. - for discovery in discoveries: - discovery[DISCOVERY_UDN] = discovery["_udn"] - discovery[DISCOVERY_ST] = discovery["st"] - discovery[DISCOVERY_LOCATION] = discovery["location"] - discovery[DISCOVERY_USN] = discovery["usn"] - _LOGGER.debug("Discovered device: %s", discovery) - + discoveries = [] + for ssdp_st in IgdDevice.DEVICE_TYPES: + for discovery_info in ssdp.async_get_discovery_info_by_st(hass, ssdp_st): + discoveries.append(discovery_info_to_discovery(discovery_info)) return discoveries @classmethod diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index b130e721e35..5acd260ec9a 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.18.0"], + "dependencies": ["ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 93f21911c78..6a911f8d4db 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,7 +1,7 @@ """Test UPnP/IGD config flow.""" from datetime import timedelta -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -35,6 +35,14 @@ async def test_flow_ssdp_discovery(hass: HomeAssistant): udn = "uuid:device_1" location = "dummy" mock_device = MockDevice(udn) + ssdp_discoveries = [ + { + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + ssdp.ATTR_SSDP_USN: mock_device.usn, + } + ] discoveries = [ { DISCOVERY_LOCATION: location, @@ -49,7 +57,7 @@ async def test_flow_ssdp_discovery(hass: HomeAssistant): with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) ), patch.object( - Device, "async_discover", AsyncMock(return_value=discoveries) + ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) ), patch.object( Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) ): @@ -156,6 +164,14 @@ async def test_flow_user(hass: HomeAssistant): udn = "uuid:device_1" location = "dummy" mock_device = MockDevice(udn) + ssdp_discoveries = [ + { + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + ssdp.ATTR_SSDP_USN: mock_device.usn, + } + ] discoveries = [ { DISCOVERY_LOCATION: location, @@ -171,7 +187,7 @@ async def test_flow_user(hass: HomeAssistant): with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) ), patch.object( - Device, "async_discover", AsyncMock(return_value=discoveries) + ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) ), patch.object( Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) ): @@ -202,6 +218,14 @@ async def test_flow_import(hass: HomeAssistant): udn = "uuid:device_1" mock_device = MockDevice(udn) location = "dummy" + ssdp_discoveries = [ + { + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + ssdp.ATTR_SSDP_USN: mock_device.usn, + } + ] discoveries = [ { DISCOVERY_LOCATION: location, @@ -217,7 +241,7 @@ async def test_flow_import(hass: HomeAssistant): with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) ), patch.object( - Device, "async_discover", AsyncMock(return_value=discoveries) + ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) ), patch.object( Device, "async_supplement_discovery", AsyncMock(return_value=discoveries[0]) ): @@ -261,31 +285,19 @@ async def test_flow_import_already_configured(hass: HomeAssistant): assert result["reason"] == "already_configured" -async def test_flow_import_incomplete(hass: HomeAssistant): - """Test config flow: incomplete discovery, configured through configuration.yaml.""" - udn = "uuid:device_1" - mock_device = MockDevice(udn) - location = "dummy" - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - # DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] - - with patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)): +async def test_flow_import_no_devices_found(hass: HomeAssistant): + """Test config flow: no devices found, configured through configuration.yaml.""" + ssdp_discoveries = [] + with patch.object( + ssdp, "async_get_discovery_info_by_st", Mock(return_value=ssdp_discoveries) + ): # Discovered via step import. result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "incomplete_discovery" + assert result["reason"] == "no_devices_found" async def test_options_flow(hass: HomeAssistant): @@ -294,15 +306,12 @@ async def test_options_flow(hass: HomeAssistant): udn = "uuid:device_1" location = "http://192.168.1.1/desc.xml" mock_device = MockDevice(udn) - discoveries = [ + ssdp_discoveries = [ { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + ssdp.ATTR_SSDP_USN: mock_device.usn, } ] config_entry = MockConfigEntry( @@ -321,7 +330,11 @@ async def test_options_flow(hass: HomeAssistant): } with patch.object( Device, "async_create_device", AsyncMock(return_value=mock_device) - ), patch.object(Device, "async_discover", AsyncMock(return_value=discoveries)): + ), patch.object( + ssdp, + "async_get_discovery_info_by_udn_st", + Mock(return_value=ssdp_discoveries[0]), + ): # Initialisation of component. await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index e6e37ca52fb..0770906f0da 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,17 +1,11 @@ """Test UPnP/IGD setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch +from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, - DISCOVERY_HOSTNAME, - DISCOVERY_LOCATION, - DISCOVERY_NAME, - DISCOVERY_ST, - DISCOVERY_UDN, - DISCOVERY_UNIQUE_ID, - DISCOVERY_USN, DOMAIN, ) from homeassistant.components.upnp.device import Device @@ -28,17 +22,12 @@ async def test_async_setup_entry_default(hass: HomeAssistant): udn = "uuid:device_1" location = "http://192.168.1.1/desc.xml" mock_device = MockDevice(udn) - discoveries = [ - { - DISCOVERY_LOCATION: location, - DISCOVERY_NAME: mock_device.name, - DISCOVERY_ST: mock_device.device_type, - DISCOVERY_UDN: mock_device.udn, - DISCOVERY_UNIQUE_ID: mock_device.unique_id, - DISCOVERY_USN: mock_device.usn, - DISCOVERY_HOSTNAME: mock_device.hostname, - } - ] + discovery = { + ssdp.ATTR_SSDP_LOCATION: location, + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + ssdp.ATTR_SSDP_USN: mock_device.usn, + } entry = MockConfigEntry( domain=DOMAIN, data={ @@ -51,77 +40,19 @@ async def test_async_setup_entry_default(hass: HomeAssistant): # no upnp } async_create_device = AsyncMock(return_value=mock_device) - async_discover = AsyncMock() + mock_get_discovery = Mock() with patch.object(Device, "async_create_device", async_create_device), patch.object( - Device, "async_discover", async_discover + ssdp, "async_get_discovery_info_by_udn_st", mock_get_discovery ): # initialisation of component, no device discovered - async_discover.return_value = [] + mock_get_discovery.return_value = None await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() # loading of config_entry, device discovered - async_discover.return_value = discoveries + mock_get_discovery.return_value = discovery entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True # ensure device is stored/used - async_create_device.assert_called_with(hass, discoveries[0][DISCOVERY_LOCATION]) - - -async def test_sync_setup_entry_multiple_discoveries(hass: HomeAssistant): - """Test async_setup_entry.""" - udn_0 = "uuid:device_1" - location_0 = "http://192.168.1.1/desc.xml" - mock_device_0 = MockDevice(udn_0) - udn_1 = "uuid:device_2" - location_1 = "http://192.168.1.2/desc.xml" - mock_device_1 = MockDevice(udn_1) - discoveries = [ - { - DISCOVERY_LOCATION: location_0, - DISCOVERY_NAME: mock_device_0.name, - DISCOVERY_ST: mock_device_0.device_type, - DISCOVERY_UDN: mock_device_0.udn, - DISCOVERY_UNIQUE_ID: mock_device_0.unique_id, - DISCOVERY_USN: mock_device_0.usn, - DISCOVERY_HOSTNAME: mock_device_0.hostname, - }, - { - DISCOVERY_LOCATION: location_1, - DISCOVERY_NAME: mock_device_1.name, - DISCOVERY_ST: mock_device_1.device_type, - DISCOVERY_UDN: mock_device_1.udn, - DISCOVERY_UNIQUE_ID: mock_device_1.unique_id, - DISCOVERY_USN: mock_device_1.usn, - DISCOVERY_HOSTNAME: mock_device_1.hostname, - }, - ] - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONFIG_ENTRY_UDN: mock_device_1.udn, - CONFIG_ENTRY_ST: mock_device_1.device_type, - }, - ) - - config = { - # no upnp - } - async_create_device = AsyncMock(return_value=mock_device_1) - async_discover = AsyncMock() - with patch.object(Device, "async_create_device", async_create_device), patch.object( - Device, "async_discover", async_discover - ): - # initialisation of component, no device discovered - async_discover.return_value = [] - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() - - # loading of config_entry, device discovered - async_discover.return_value = discoveries - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) is True - - # ensure device is stored/used - async_create_device.assert_called_with(hass, discoveries[1][DISCOVERY_LOCATION]) + async_create_device.assert_called_with(hass, discovery[ssdp.ATTR_SSDP_LOCATION]) From d0fa4e1d48d1a227b53d352ac68f56817dd5ee73 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Jun 2021 22:38:20 +0200 Subject: [PATCH 254/750] Upgrade wled to 0.5.0 (#51632) --- homeassistant/components/wled/light.py | 14 +++++++------- homeassistant/components/wled/manifest.json | 2 +- homeassistant/components/wled/sensor.py | 16 ++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 20960ad0bb7..d9240e75efb 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -124,7 +124,7 @@ class WLEDMasterLight(WLEDEntity, LightEntity): # WLED uses 100ms per unit, so 10 = 1 second. data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) - await self.coordinator.wled.master(**data) + await self.coordinator.wled.master(**data) # type: ignore[arg-type] @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: @@ -138,7 +138,7 @@ class WLEDMasterLight(WLEDEntity, LightEntity): if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] - await self.coordinator.wled.master(**data) + await self.coordinator.wled.master(**data) # type: ignore[arg-type] async def async_effect( self, @@ -195,11 +195,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" - playlist = self.coordinator.data.state.playlist + playlist: int | None = self.coordinator.data.state.playlist if playlist == -1: playlist = None - preset = self.coordinator.data.state.preset + preset: int | None = self.coordinator.data.state.preset if preset == -1: preset = None @@ -287,11 +287,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # If there is a single segment, control via the master if len(self.coordinator.data.state.segments) == 1: - await self.coordinator.wled.master(**data) + await self.coordinator.wled.master(**data) # type: ignore[arg-type] return data[ATTR_SEGMENT_ID] = self._segment - await self.coordinator.wled.segment(**data) + await self.coordinator.wled.segment(**data) # type: ignore[arg-type] @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: @@ -389,7 +389,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if speed is not None: data[ATTR_SPEED] = speed - await self.coordinator.wled.segment(**data) + await self.coordinator.wled.segment(**data) # type: ignore[arg-type] @wled_exception_handler async def async_preset( diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index b0768897076..b5ac91a1bf8 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.4.4"], + "requirements": ["wled==0.5.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 73c012f25c7..f6b9b0d973a 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -121,8 +121,10 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" @property - def state(self) -> int: + def state(self) -> int | None: """Return the state of the sensor.""" + if not self.coordinator.data.info.wifi: + return None return self.coordinator.data.info.wifi.signal @@ -140,8 +142,10 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" @property - def state(self) -> int: + def state(self) -> int | None: """Return the state of the sensor.""" + if not self.coordinator.data.info.wifi: + return None return self.coordinator.data.info.wifi.rssi @@ -158,8 +162,10 @@ class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" @property - def state(self) -> int: + def state(self) -> int | None: """Return the state of the sensor.""" + if not self.coordinator.data.info.wifi: + return None return self.coordinator.data.info.wifi.channel @@ -176,6 +182,8 @@ class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" @property - def state(self) -> str: + def state(self) -> str | None: """Return the state of the sensor.""" + if not self.coordinator.data.info.wifi: + return None return self.coordinator.data.info.wifi.bssid diff --git a/requirements_all.txt b/requirements_all.txt index 509c4133b82..6b554284c87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2359,7 +2359,7 @@ wirelesstagpy==0.4.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.4.4 +wled==0.5.0 # homeassistant.components.wolflink wolf_smartset==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ec4558d80d..eaf2cf98fb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1274,7 +1274,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.4.4 +wled==0.5.0 # homeassistant.components.wolflink wolf_smartset==0.1.8 From 6ed671dfda972a1c0d92f4e19f917a5caad6332b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 9 Jun 2021 00:10:10 +0000 Subject: [PATCH 255/750] [ci skip] Translation update --- .../components/apple_tv/translations/de.json | 2 +- .../components/arcam_fmj/translations/de.json | 2 +- .../components/axis/translations/de.json | 2 +- .../azure_devops/translations/de.json | 2 +- .../components/blebox/translations/de.json | 2 +- .../components/blink/translations/de.json | 2 +- .../components/bond/translations/de.json | 2 +- .../components/brother/translations/de.json | 2 +- .../components/bsblan/translations/de.json | 2 +- .../components/canary/translations/de.json | 2 +- .../components/cast/translations/de.json | 2 +- .../cloudflare/translations/de.json | 2 +- .../components/denonavr/translations/de.json | 2 +- .../components/directv/translations/de.json | 2 +- .../components/doorbird/translations/de.json | 2 +- .../components/elgato/translations/de.json | 4 +-- .../components/esphome/translations/de.json | 2 +- .../forked_daapd/translations/de.json | 2 +- .../components/goalzero/translations/de.json | 4 +-- .../components/harmony/translations/de.json | 2 +- .../components/homekit/translations/de.json | 4 +-- .../homekit_controller/translations/de.json | 4 +-- .../huawei_lte/translations/de.json | 2 +- .../components/ipp/translations/de.json | 2 +- .../components/isy994/translations/de.json | 2 +- .../components/kodi/translations/de.json | 2 +- .../components/konnected/translations/de.json | 2 +- .../litterrobot/translations/de.json | 2 +- .../logi_circle/translations/de.json | 2 +- .../lutron_caseta/translations/de.json | 2 +- .../modern_forms/translations/ca.json | 28 +++++++++++++++++++ .../modern_forms/translations/de.json | 28 +++++++++++++++++++ .../modern_forms/translations/en.json | 2 +- .../modern_forms/translations/et.json | 28 +++++++++++++++++++ .../modern_forms/translations/nl.json | 28 +++++++++++++++++++ .../modern_forms/translations/no.json | 28 +++++++++++++++++++ .../modern_forms/translations/ru.json | 28 +++++++++++++++++++ .../components/nws/translations/de.json | 2 +- .../components/nzbget/translations/de.json | 2 +- .../ovo_energy/translations/de.json | 2 +- .../components/plaato/translations/de.json | 2 +- .../components/plugwise/translations/de.json | 4 +-- .../components/powerwall/translations/de.json | 2 +- .../components/ps4/translations/de.json | 2 +- .../components/roku/translations/de.json | 2 +- .../components/roomba/translations/de.json | 4 +-- .../components/smappee/translations/de.json | 2 +- .../components/smarttub/translations/de.json | 2 +- .../somfy_mylink/translations/de.json | 2 +- .../components/sonarr/translations/de.json | 2 +- .../components/songpal/translations/de.json | 2 +- .../components/starline/translations/de.json | 2 +- .../components/syncthru/translations/de.json | 2 +- .../synology_dsm/translations/de.json | 2 +- .../components/unifi/translations/de.json | 2 +- .../components/upnp/translations/de.json | 2 +- .../components/vera/translations/de.json | 4 +-- .../components/wilight/translations/de.json | 2 +- .../components/withings/translations/de.json | 2 +- .../components/wled/translations/de.json | 2 +- .../xiaomi_aqara/translations/de.json | 2 +- .../xiaomi_miio/translations/de.json | 2 +- .../components/zwave/translations/de.json | 2 +- 63 files changed, 232 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/modern_forms/translations/ca.json create mode 100644 homeassistant/components/modern_forms/translations/de.json create mode 100644 homeassistant/components/modern_forms/translations/et.json create mode 100644 homeassistant/components/modern_forms/translations/nl.json create mode 100644 homeassistant/components/modern_forms/translations/no.json create mode 100644 homeassistant/components/modern_forms/translations/ru.json diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index 464bad99d5a..6161550d736 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -16,7 +16,7 @@ "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!", diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index 1f67a8d30a9..684a2f18961 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -5,7 +5,7 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Arcam FMJ auf {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?" diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index ed95dea6fc1..607000b6eaa 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -11,7 +11,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, - "flow_title": "Achsenger\u00e4t: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index 43a5776da2e..eabc88625fb 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -9,7 +9,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "project_error": "Konnte keine Projektinformationen erhalten." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index 37c8dde54e5..508e4b66ee6 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -9,7 +9,7 @@ "unknown": "Unerwarteter Fehler", "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." }, - "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index 86fa2b609ad..8d3911d5f80 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -14,7 +14,7 @@ "data": { "2fa": "Zwei-Faktor Authentifizierungscode" }, - "description": "Gib die an deine E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lass das Feld leer.", + "description": "Gib die an deine E-Mail gesendete Pin ein", "title": "Zwei-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 4b7372a4526..934e166e0d5 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -9,7 +9,7 @@ "old_firmware": "Nicht unterst\u00fctzte alte Firmware auf dem Bond-Ger\u00e4t - bitte aktualisiere, bevor du fortf\u00e4hrst", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index c2a7ae8ec76..3390ca6ca8f 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -9,7 +9,7 @@ "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, - "flow_title": "Brother-Drucker: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index d1400529b0b..ce9d8a0cb00 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index 081eb9126df..93ce43c61f5 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 1358ab16210..4d029be9603 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -29,7 +29,7 @@ "ignore_cec": "CEC ignorieren", "uuid": "Zul\u00e4ssige UUIDs" }, - "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nIgnore CEC - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.", + "description": "Erlaubte UUIDs - Eine kommagetrennte Liste von UUIDs von Cast-Ger\u00e4ten, die dem Home Assistant hinzugef\u00fcgt werden sollen. Nur verwenden, wenn Sie nicht alle verf\u00fcgbaren Cast-Ger\u00e4te hinzuf\u00fcgen m\u00f6chten.\nCEC ignorieren - Eine kommagetrennte Liste von Chromecasts, die CEC-Daten zur Bestimmung des aktiven Eingangs ignorieren sollen. Dies wird an pychromecast.IGNORE_CEC \u00fcbergeben.", "title": "Erweiterte Google Cast-Konfiguration" }, "basic_options": { diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 21118e106bf..5bd4631d5b3 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -9,7 +9,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_zone": "Ung\u00fcltige Zone" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index f8896e5a08f..f40c665489c 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden" }, - "flow_title": "Denon AVR-Netzwerk-Receiver: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers", diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json index 95bd807c048..de5cc512940 100644 --- a/homeassistant/components/directv/translations/de.json +++ b/homeassistant/components/directv/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "data": { diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 0d6bef7a63f..640f13a73c6 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -10,7 +10,7 @@ "invalid_auth": "Ung\u00fcltige Authentifikation", "unknown": "Unerwarteter Fehler" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index cb7fb18f003..95bb2609d84 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { "host": "Host", "port": "Port" }, - "description": "Richten dein Elgato Key Light f\u00fcr die Integration mit Home Assistant ein." + "description": "Richte deinen Elgato Key Light f\u00fcr die Integration mit Home Assistant ein." }, "zeroconf_confirm": { "description": "M\u00f6chtest du das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?", diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index fdaea452c45..c82afc78851 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -9,7 +9,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 0001157ce41..2047414b168 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -12,7 +12,7 @@ "wrong_password": "Ung\u00fcltiges Passwort", "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." }, - "flow_title": "Forked-Daapd-Server: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index f740d6fdfec..6f6eb052589 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unknown": "Unerwarteter Fehler" }, @@ -20,7 +20,7 @@ "host": "Host", "name": "Name" }, - "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach.", + "description": "Zuerst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/ \n\nFolge den Anweisungen, um deinen Yeti mit deinem WLAN-Netzwerk zu verbinden. Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn es nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage dazu im Benutzerhandbuch deines Routers nach.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json index 9cd07f09529..5083ccd848f 100644 --- a/homeassistant/components/harmony/translations/de.json +++ b/homeassistant/components/harmony/translations/de.json @@ -7,7 +7,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "M\u00f6chten Sie {name} ({host}) einrichten?", diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 09a6b059ea7..e115c932ac4 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -13,7 +13,7 @@ "include_domains": "Einzubeziehende Domains" }, "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", - "title": "HomeKit aktivieren" + "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus." } } }, @@ -47,7 +47,7 @@ "mode": "Modus" }, "description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.", - "title": "W\u00e4hle die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." + "title": "W\u00e4hle die zu einzubeziehenden Dom\u00e4nen aus." }, "yaml": { "description": "Dieser Eintrag wird \u00fcber YAML gesteuert", diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index e8d3b4b55e4..7df1d0fc1a7 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -18,7 +18,7 @@ "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", "unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen." }, - "flow_title": "HomeKit-Zubeh\u00f6r: {name}", + "flow_title": "{name}", "step": { "busy_error": { "description": "Brechen Sie das Pairing auf allen Controllern ab oder versuchen Sie, das Ger\u00e4t neu zu starten, und fahren Sie dann fort, das Pairing fortzusetzen.", @@ -44,7 +44,7 @@ "data": { "device": "Ger\u00e4t" }, - "description": "W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest", + "description": "HomeKit Controller kommuniziert \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest", "title": "Ger\u00e4teauswahl" } } diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 10a7af41a3c..a979eeb89fe 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -15,7 +15,7 @@ "response_error": "Unbekannter Fehler vom Ger\u00e4t", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 69402c8fdba..80497c0c874 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -13,7 +13,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." }, - "flow_title": "Drucker: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index cf658384184..a6e9e0a1498 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -9,7 +9,7 @@ "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Universalger\u00e4te ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 6a137ac4350..80d47751006 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -12,7 +12,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 69d483e92c9..98862e85a8b 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -7,7 +7,7 @@ "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden." + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "confirm": { diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index 0eee2778d05..c8f4f35716e 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Konto ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json index 1eec1d3c4a5..b1f318a8e5f 100644 --- a/homeassistant/components/logi_circle/translations/de.json +++ b/homeassistant/components/logi_circle/translations/de.json @@ -13,7 +13,7 @@ }, "step": { "auth": { - "description": "Folge dem Link unten und klicke Akzeptieren um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link] ({authorization_url})", + "description": "Folge dem Link unten und klicke **Akzeptieren** um auf dein Logi Circle-Konto zuzugreifen. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden** . \n\n [Link] ({authorization_url})", "title": "Authentifizierung mit Logi Circle" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 49cd36628ee..e87d4cc0bdb 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Konnte die aus configuration.yaml importierte Bridge (Host: {host}) nicht einrichten.", diff --git a/homeassistant/components/modern_forms/translations/ca.json b/homeassistant/components/modern_forms/translations/ca.json new file mode 100644 index 00000000000..cea3bc7b685 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura el teu ventilador Modern Forms per integrar-lo a Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vols afegir el ventilador de Modern Forms anomenat `{name}` a Home Assistant?", + "title": "Dispositiu ventilador Modern Forms descobert" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json new file mode 100644 index 00000000000..644f525179e --- /dev/null +++ b/homeassistant/components/modern_forms/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Einrichten Ihres Modern Forms Ventilator f\u00fcr die Integration in Home Assistant." + }, + "zeroconf_confirm": { + "description": "M\u00f6chten Sie den Modern Forms Ventilator mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", + "title": "Erkannter Modern Forms Ventilator" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/en.json b/homeassistant/components/modern_forms/translations/en.json index f25f5124ab4..6c51e42a2d1 100644 --- a/homeassistant/components/modern_forms/translations/en.json +++ b/homeassistant/components/modern_forms/translations/en.json @@ -25,4 +25,4 @@ } }, "title": "Modern Forms" -} +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/et.json b/homeassistant/components/modern_forms/translations/et.json new file mode 100644 index 00000000000..ee325440c13 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Seadista Modern Forms'i ventilaator sidumiseks Home Assistantiga." + }, + "zeroconf_confirm": { + "description": "Kas lisada Modern Forms'i ventilaator nimega `{nimi}` Home Assistanti?", + "title": "Leitud Modern Forms ventilaator" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/nl.json b/homeassistant/components/modern_forms/translations/nl.json new file mode 100644 index 00000000000..5a3d63e15a7 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Stel uw Modern Forms ventilator in om te integreren met Home Assistant." + }, + "zeroconf_confirm": { + "description": "Wil je de Modern Forms-fan met de naam ` {name} ` toevoegen aan Home Assistant?", + "title": "Ontdekt Modern Forms ventilator apparaat" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/no.json b/homeassistant/components/modern_forms/translations/no.json new file mode 100644 index 00000000000..04718b9d039 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Sett opp Modern Forms-fanen din for \u00e5 integrere med Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vil du legge til Modern Forms-viften med navnet {name} i Hjemmeassistent?", + "title": "Oppdaget Modern Forms-vifteenhet" + } + } + }, + "title": "Moderne former" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ru.json b/homeassistant/components/modern_forms/translations/ru.json new file mode 100644 index 00000000000..9bfba30bf33 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "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 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u043c Modern Forms." + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440 Modern Forms `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Modern Forms" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json index 3d409bf885b..b6899e34789 100644 --- a/homeassistant/components/nws/translations/de.json +++ b/homeassistant/components/nws/translations/de.json @@ -15,7 +15,7 @@ "longitude": "L\u00e4ngengrad", "station": "METAR Stationscode" }, - "description": "Wenn kein METAR-Stationscode angegeben ist, werden L\u00e4ngen- und Breitengrad verwendet, um die n\u00e4chstgelegene Station zu finden.", + "description": "Wenn kein METAR-Stationscode angegeben wird, werden der Breiten- und L\u00e4ngengrad verwendet, um die n\u00e4chstgelegene Station zu finden. Im Moment kann ein API-Schl\u00fcssel alles sein. Es wird empfohlen, eine g\u00fcltige E-Mail-Adresse zu verwenden.", "title": "Stellen Sie eine Verbindung zum Nationalen Wetterdienst her" } } diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 529eff3d9a2..74d073ce292 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 6fccec14333..de86a7adf14 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -5,7 +5,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index fc5718cacb2..8d13e5d8cb0 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -28,7 +28,7 @@ "device_type": "Art des Plaato-Ger\u00e4ts" }, "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", - "title": "Plaato Webhook einrichten" + "title": "Plaato Ger\u00e4te einrichten" }, "webhook": { "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url}).", diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 97587131774..9e2836202df 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -8,13 +8,13 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { "flow_type": "Verbindungstyp" }, - "description": "Details", + "description": "Produkt:", "title": "Plugwise Typ" }, "user_gateway": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 02ad906a568..88b0473232f 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -10,7 +10,7 @@ "unknown": "Unerwarteter Fehler", "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index d5aa867f1db..1a20740dfb1 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 152161cb27f..77cd1b94b6e 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "M\u00f6chtest du {name} einrichten?", diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 3067c968ff3..053f3a31567 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -19,7 +19,7 @@ "title": "Automatisch mit dem Ger\u00e4t verbinden" }, "link": { - "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden).", + "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden) und sende die Best\u00e4tigung innerhalb von 30 Sekunden ab.", "title": "Passwort abrufen" }, "link_manual": { diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 6491fbf2d15..121b74e9627 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -9,7 +9,7 @@ "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json index 0e399b9d018..4549360f761 100644 --- a/homeassistant/components/smarttub/translations/de.json +++ b/homeassistant/components/smarttub/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_configured": "Konto ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index d66be000314..d88d1320279 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -8,7 +8,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index b4ceeeb43b7..c7ca7bd692b 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -9,7 +9,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Die Sonarr-Integration muss manuell mit der Sonarr-API, die unter {host} gehostet wird, neu authentifiziert werden", diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json index 851e0b5f77c..b9b7fae3f28 100644 --- a/homeassistant/components/songpal/translations/de.json +++ b/homeassistant/components/songpal/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "M\u00f6chten Sie {name} ({host}) einrichten?" diff --git a/homeassistant/components/starline/translations/de.json b/homeassistant/components/starline/translations/de.json index 5788b570eba..87a9249475e 100644 --- a/homeassistant/components/starline/translations/de.json +++ b/homeassistant/components/starline/translations/de.json @@ -11,7 +11,7 @@ "app_id": "App-ID", "app_secret": "Geheimnis" }, - "description": "Anwendungs-ID und Geheimcode aus dem StarLine-Entwicklerkonto", + "description": "Anwendungs-ID und Geheimcode aus dem [StarLine Entwicklerkonto](https://my.starline.ru/developer)", "title": "Anmeldeinformationen der Anwendung" }, "auth_captcha": { diff --git a/homeassistant/components/syncthru/translations/de.json b/homeassistant/components/syncthru/translations/de.json index 450d0466597..f7533630216 100644 --- a/homeassistant/components/syncthru/translations/de.json +++ b/homeassistant/components/syncthru/translations/de.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Ger\u00e4t unterst\u00fctzt kein SyncThru", "unknown_state": "Druckerstatus unbekannt, \u00fcberpr\u00fcfe URL und Netzwerkverbindung" }, - "flow_title": "Samsung SyncThru Drucker: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index f0d274c3bfe..932cc42db1d 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -10,7 +10,7 @@ "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 39ac9e5c946..f1f2fdd3627 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -10,7 +10,7 @@ "service_unavailable": "Verbindung fehlgeschlagen", "unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar." }, - "flow_title": "UniFi Netzwerk {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index 2d2a97b0943..a43700ba236 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -9,7 +9,7 @@ "one": "Ein", "other": "andere" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "M\u00f6chten Sie dieses UPnP/IGD-Ger\u00e4t einrichten?" diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json index f0a680e7c2d..7fce8f6bdfe 100644 --- a/homeassistant/components/vera/translations/de.json +++ b/homeassistant/components/vera/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "Konnte keine Verbindung zum Controller mit url {base_url} herstellen" + "cannot_connect": "Konnte keine Verbindung zum Controller mit URL {base_url} herstellen" }, "step": { "user": { @@ -10,7 +10,7 @@ "lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen.", "vera_controller_url": "Controller-URL" }, - "description": "Stellen Sie unten eine Vera-Controller-Url zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", + "description": "Stellen Sie unten eine Vera-Controller-URL zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", "title": "Richten Sie den Vera-Controller ein" } } diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index f79d960c182..851defe0e32 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -5,7 +5,7 @@ "not_supported_device": "Dieses WiLight wird derzeit nicht unterst\u00fctzt", "not_wilight_device": "Dieses Ger\u00e4t ist kein WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "M\u00f6chten Sie WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 7d0aead7a66..31d5ad2f6e5 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Konto wurde bereits konfiguriert" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index 0dd13f763d6..d700e79e0b6 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index e72f00d5f5f..87120f09605 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -12,7 +12,7 @@ "invalid_key": "Ung\u00fcltiger Gateway-Schl\u00fcssel", "invalid_mac": "Ung\u00fcltige MAC-Adresse" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 2817d18b578..ef5c6c35a37 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -9,7 +9,7 @@ "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index 488580cb18a..0a82d5b0bc7 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -13,7 +13,7 @@ "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", "usb_path": "USB-Ger\u00e4t Pfad" }, - "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/" + "description": "Diese Integration wird nicht mehr gepflegt. Verwenden Sie bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" } } }, From a1e3283f8fb26002f5e92cd5c002b8ba4b079e72 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Jun 2021 08:28:08 +0200 Subject: [PATCH 256/750] Improve editing of device automations referencing non-added sensors (#51312) --- .../components/sensor/device_trigger.py | 29 ++++++-------- homeassistant/helpers/entity.py | 34 +++++++++++++++++ .../components/sensor/test_device_trigger.py | 38 +++++++++++++++---- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 0bca1e299d6..9729d629d1c 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -9,8 +9,6 @@ from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -30,7 +28,9 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ) +from homeassistant.core import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN @@ -134,18 +134,12 @@ async def async_get_triggers(hass, device_id): ] for entry in entries: - device_class = DEVICE_CLASS_NONE - state = hass.states.get(entry.entity_id) - unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None - ) + device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE + unit_of_measurement = get_unit_of_measurement(hass, entry.entity_id) - if not state or not unit_of_measurement: + if not unit_of_measurement: continue - if ATTR_DEVICE_CLASS in state.attributes: - device_class = state.attributes[ATTR_DEVICE_CLASS] - templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] ) @@ -166,15 +160,14 @@ async def async_get_triggers(hass, device_id): async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) - unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None - ) + try: + unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID]) + except HomeAssistantError: + unit_of_measurement = None - if not state or not unit_of_measurement: + if not unit_of_measurement: raise InvalidDeviceAutomationConfig( - "No state or unit of measurement found for " - f"trigger entity {config[CONF_ENTITY_ID]}" + f"No unit of measurement found for trigger entity {config[CONF_ENTITY_ID]}" ) return { diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4afab38fabf..dce68c80871 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -93,6 +93,23 @@ def async_generate_entity_id( return test_string +def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None: + """Get device class of an entity. + + First try the statemachine, then entity registry. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(ATTR_DEVICE_CLASS) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + + return entry.device_class + + def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: """Get supported features for an entity. @@ -110,6 +127,23 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: return entry.supported_features or 0 +def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: + """Get unit of measurement class of an entity. + + First try the statemachine, then entity registry. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + + return entry.unit_of_measurement + + class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 9da93510523..ce35e2506a9 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -6,7 +6,12 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import ( + CONF_PLATFORM, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + STATE_UNKNOWN, +) from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -85,8 +90,22 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat assert triggers == expected_triggers +@pytest.mark.parametrize( + "set_state,device_class_reg,device_class_state,unit_reg,unit_state", + [ + (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None), + (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE), + ], +) async def test_get_trigger_capabilities( - hass, device_reg, entity_reg, enable_custom_integrations + hass, + device_reg, + entity_reg, + set_state, + device_class_reg, + device_class_state, + unit_reg, + unit_state, ): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -98,15 +117,20 @@ async def test_get_trigger_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create( + entity_id = entity_reg.async_get_or_create( DOMAIN, "test", platform.ENTITIES["battery"].unique_id, device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() + device_class=device_class_reg, + unit_of_measurement=unit_reg, + ).entity_id + if set_state: + hass.states.async_set( + entity_id, + None, + {"device_class": device_class_state, "unit_of_measurement": unit_state}, + ) expected_capabilities = { "extra_fields": [ From 062e2bab67c639f6864c4479bf6b04ea68a9231e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 09:27:24 +0200 Subject: [PATCH 257/750] Bump codecov/codecov-action from 1.5.0 to 1.5.2 (#51652) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f6518f67ce2..8f2242dd90d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -740,4 +740,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.5.0 + uses: codecov/codecov-action@v1.5.2 From 443463e19d74dc1362b58c26d03bdfced08454ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Jun 2021 11:23:01 +0200 Subject: [PATCH 258/750] Emulate color_temp for lights which support color or white (#51654) * Emulate color_temp for lights which support color or white * Support legacy lights * Tidy up group.light code * Improve color_temp to white conversion * Remove color_temp to white conversion * Add test * Tweak deconz test --- homeassistant/components/group/light.py | 61 +++------------------- homeassistant/components/light/__init__.py | 16 ++++-- tests/components/deconz/test_light.py | 1 - tests/components/light/test_init.py | 60 +++++++++++++++++++++ 4 files changed, 79 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 9567735c9eb..0abe842af2c 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,7 +1,6 @@ """This platform allows several lights to be grouped into one light.""" from __future__ import annotations -import asyncio from collections import Counter from collections.abc import Iterator import itertools @@ -34,8 +33,6 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - color_supported, - color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -49,7 +46,6 @@ from homeassistant.core import CoreState, HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType -from homeassistant.util import color as color_util from . import GroupEntity @@ -233,7 +229,6 @@ class LightGroup(GroupEntity, light.LightEntity): async def async_turn_on(self, **kwargs): """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} - emulate_color_temp_entity_ids = [] if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] @@ -256,21 +251,6 @@ class LightGroup(GroupEntity, light.LightEntity): if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] - # Create a new entity list to mutate - updated_entities = list(self._entity_ids) - - # Walk through initial entity ids, split entity lists by support - for entity_id in self._entity_ids: - state = self.hass.states.get(entity_id) - if not state: - continue - support = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - # Only pass color temperature to supported entity_ids - if color_supported(support) and not color_temp_supported(support): - emulate_color_temp_entity_ids.append(entity_id) - updated_entities.remove(entity_id) - data[ATTR_ENTITY_ID] = updated_entities - if ATTR_WHITE_VALUE in kwargs: data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE] @@ -283,41 +263,12 @@ class LightGroup(GroupEntity, light.LightEntity): if ATTR_FLASH in kwargs: data[ATTR_FLASH] = kwargs[ATTR_FLASH] - if not emulate_color_temp_entity_ids: - await self.hass.services.async_call( - light.DOMAIN, - light.SERVICE_TURN_ON, - data, - blocking=True, - context=self._context, - ) - return - - emulate_color_temp_data = data.copy() - temp_k = color_util.color_temperature_mired_to_kelvin( - emulate_color_temp_data[ATTR_COLOR_TEMP] - ) - hs_color = color_util.color_temperature_to_hs(temp_k) - emulate_color_temp_data[ATTR_HS_COLOR] = hs_color - del emulate_color_temp_data[ATTR_COLOR_TEMP] - - emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids - - await asyncio.gather( - self.hass.services.async_call( - light.DOMAIN, - light.SERVICE_TURN_ON, - data, - blocking=True, - context=self._context, - ), - self.hass.services.async_call( - light.DOMAIN, - light.SERVICE_TURN_ON, - emulate_color_temp_data, - blocking=True, - context=self._context, - ), + await self.hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, ) async def async_turn_off(self, **kwargs): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 32494592697..a4693857178 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -370,13 +370,13 @@ async def async_setup(hass, config): # noqa: C901 ): profiles.apply_default(light.entity_id, light.is_on, params) + legacy_supported_color_modes = ( + light._light_internal_supported_color_modes # pylint: disable=protected-access + ) supported_color_modes = light.supported_color_modes # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W # for legacy lights if ATTR_RGBW_COLOR in params: - legacy_supported_color_modes = ( - light._light_internal_supported_color_modes # pylint: disable=protected-access - ) if ( COLOR_MODE_RGBW in legacy_supported_color_modes and not supported_color_modes @@ -385,6 +385,16 @@ async def async_setup(hass, config): # noqa: C901 params[ATTR_RGB_COLOR] = rgbw_color[0:3] params[ATTR_WHITE_VALUE] = rgbw_color[3] + # If a color temperature is specified, emulate it if not supported by the light + if ( + ATTR_COLOR_TEMP in params + and COLOR_MODE_COLOR_TEMP not in legacy_supported_color_modes + ): + color_temp = params.pop(ATTR_COLOR_TEMP) + if color_supported(legacy_supported_color_modes): + temp_k = color_util.color_temperature_mired_to_kelvin(color_temp) + params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(temp_k) + # If a color is specified, convert to the color space supported by the light # Backwards compatibility: Fall back to hs color if light.supported_color_modes # is not implemented diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index a5e27709ebf..6ec73085322 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -170,7 +170,6 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.rgb_light", - ATTR_COLOR_TEMP: 2500, ATTR_BRIGHTNESS: 200, ATTR_TRANSITION: 5, ATTR_FLASH: FLASH_SHORT, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 368a5b0dab4..b3682cbdd2c 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1586,6 +1586,66 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} +async def test_light_service_call_color_temp_emulation( + hass, enable_custom_integrations +): + """Test color conversion in service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs_ct", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_hs_white", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_COLOR_TEMP, light.COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_HS} + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_HS, light.COLOR_MODE_WHITE} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + ] + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_WHITE, + ] + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + ], + "brightness_pct": 100, + "color_temp": 200, + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "color_temp": 200} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (27.001, 19.243)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (27.001, 19.243)} + + async def test_light_service_call_white_mode(hass, enable_custom_integrations): """Test color_mode white in service calls.""" platform = getattr(hass.components, "test.light") From 4e0c9dd18c955ffa9d0e51b5574dfa018873195a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 9 Jun 2021 11:52:21 +0200 Subject: [PATCH 259/750] Increase test coverage in Brother integration (#51657) --- homeassistant/components/brother/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 353c8b05ed5..08daf0155a1 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -30,9 +30,9 @@ def host_valid(host: str) -> bool: if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) - return False + pass + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): From f09f47f13ab20d0a5fedee5b8ae2379caf7b0358 Mon Sep 17 00:00:00 2001 From: cklagenberg <32219408+cklagenberg@users.noreply.github.com> Date: Wed, 9 Jun 2021 12:06:02 +0200 Subject: [PATCH 260/750] Add device trigger support for Philips Hue Wall Switch Module (#51574) --- homeassistant/components/hue/device_trigger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index e65b362f9e7..a5df9d60985 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -55,6 +55,12 @@ HUE_BUTTON_REMOTE = { (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, } +HUE_WALL_REMOTE_MODEL = "Hue wall switch module" # ZLLSWITCH/RDM001 +HUE_WALL_REMOTE = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, +} + HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH HUE_TAP_REMOTE = { (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, @@ -84,6 +90,7 @@ REMOTES = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, + HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE, HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE, } From 87813ea991db9b397e694ae63b8061dd2e2529fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Jun 2021 12:53:08 +0200 Subject: [PATCH 261/750] Tweak light.valid_supported_color_modes (#51659) --- homeassistant/components/light/__init__.py | 2 +- tests/components/light/test_init.py | 39 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a4693857178..ce86ae4e257 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -92,7 +92,7 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]: or COLOR_MODE_UNKNOWN in color_modes or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1) or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1) - or (COLOR_MODE_WHITE in color_modes and len(color_modes) == 1) + or (COLOR_MODE_WHITE in color_modes and not color_supported(color_modes)) ): raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}") return color_modes diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index b3682cbdd2c..d9394ae946e 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1943,3 +1943,42 @@ async def test_services_filter_parameters( _, data = ent1.last_call("turn_off") assert data == {} + + +def test_valid_supported_color_modes(): + """Test valid_supported_color_modes.""" + supported = {light.COLOR_MODE_HS} + assert light.valid_supported_color_modes(supported) == supported + + # Supported color modes must not be empty + supported = set() + with pytest.raises(vol.Error): + light.valid_supported_color_modes(supported) + + # COLOR_MODE_WHITE must be combined with a color mode supporting color + supported = {light.COLOR_MODE_WHITE} + with pytest.raises(vol.Error): + light.valid_supported_color_modes(supported) + + supported = {light.COLOR_MODE_WHITE, light.COLOR_MODE_COLOR_TEMP} + with pytest.raises(vol.Error): + light.valid_supported_color_modes(supported) + + supported = {light.COLOR_MODE_WHITE, light.COLOR_MODE_HS} + assert light.valid_supported_color_modes(supported) == supported + + # COLOR_MODE_ONOFF must be the only supported mode + supported = {light.COLOR_MODE_ONOFF} + assert light.valid_supported_color_modes(supported) == supported + + supported = {light.COLOR_MODE_ONOFF, light.COLOR_MODE_COLOR_TEMP} + with pytest.raises(vol.Error): + light.valid_supported_color_modes(supported) + + # COLOR_MODE_BRIGHTNESS must be the only supported mode + supported = {light.COLOR_MODE_BRIGHTNESS} + assert light.valid_supported_color_modes(supported) == supported + + supported = {light.COLOR_MODE_BRIGHTNESS, light.COLOR_MODE_COLOR_TEMP} + with pytest.raises(vol.Error): + light.valid_supported_color_modes(supported) From d021e593d36048b2c36ccc38a2628a24477ad23a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Jun 2021 13:22:37 +0200 Subject: [PATCH 262/750] Add Ambee integration (#51645) --- .coveragerc | 2 + .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/ambee/__init__.py | 46 +++++++ homeassistant/components/ambee/config_flow.py | 70 ++++++++++ homeassistant/components/ambee/const.py | 69 ++++++++++ homeassistant/components/ambee/manifest.json | 9 ++ homeassistant/components/ambee/sensor.py | 69 ++++++++++ homeassistant/components/ambee/strings.json | 19 +++ .../components/ambee/translations/en.json | 19 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ambee/__init__.py | 1 + tests/components/ambee/test_config_flow.py | 129 ++++++++++++++++++ 16 files changed, 453 insertions(+) create mode 100644 homeassistant/components/ambee/__init__.py create mode 100644 homeassistant/components/ambee/config_flow.py create mode 100644 homeassistant/components/ambee/const.py create mode 100644 homeassistant/components/ambee/manifest.json create mode 100644 homeassistant/components/ambee/sensor.py create mode 100644 homeassistant/components/ambee/strings.json create mode 100644 homeassistant/components/ambee/translations/en.json create mode 100644 tests/components/ambee/__init__.py create mode 100644 tests/components/ambee/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 9437f8943a3..1d2c6275fc3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -45,6 +45,8 @@ omit = homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/* + homeassistant/components/ambee/__init__.py + homeassistant/components/ambee/sensor.py homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/* homeassistant/components/amcrest/* diff --git a/.strict-typing b/.strict-typing index 2ad7d0d1107..b44b04dd606 100644 --- a/.strict-typing +++ b/.strict-typing @@ -12,6 +12,7 @@ homeassistant.components.airly.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* +homeassistant.components.ambee.* homeassistant.components.ampio.* homeassistant.components.automation.* homeassistant.components.binary_sensor.* diff --git a/CODEOWNERS b/CODEOWNERS index 6471d547be3..0a5c9503dba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -33,6 +33,7 @@ homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff +homeassistant/components/ambee/* @frenck homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya homeassistant/components/analytics/* @home-assistant/core @ludeeus diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py new file mode 100644 index 00000000000..cea586d1a67 --- /dev/null +++ b/homeassistant/components/ambee/__init__.py @@ -0,0 +1,46 @@ +"""Support for Ambee.""" +from __future__ import annotations + +from ambee import Ambee + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = (SENSOR_DOMAIN,) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ambee from a config entry.""" + client = Ambee( + api_key=entry.data[CONF_API_KEY], + latitude=entry.data[CONF_LATITUDE], + longitude=entry.data[CONF_LONGITUDE], + ) + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + update_method=client.air_quality, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Ambee config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py new file mode 100644 index 00000000000..78b25e6226c --- /dev/null +++ b/homeassistant/components/ambee/config_flow.py @@ -0,0 +1,70 @@ +"""Config flow to configure the Ambee integration.""" +from __future__ import annotations + +from typing import Any + +from ambee import Ambee, AmbeeAuthenticationError, AmbeeError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + + +class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Ambee.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + session = async_get_clientsession(self.hass) + try: + client = Ambee( + api_key=user_input[CONF_API_KEY], + latitude=user_input[CONF_LATITUDE], + longitude=user_input[CONF_LONGITUDE], + session=session, + ) + await client.air_quality() + except AmbeeAuthenticationError: + errors["base"] = "invalid_api_key" + except AmbeeError: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_API_KEY: user_input[CONF_API_KEY], + CONF_LATITUDE: user_input[CONF_LATITUDE], + CONF_LONGITUDE: user_input[CONF_LONGITUDE], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py new file mode 100644 index 00000000000..56107131f34 --- /dev/null +++ b/homeassistant/components/ambee/const.py @@ -0,0 +1,69 @@ +"""Constants for the Ambee integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, Final + +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_NAME, + ATTR_SERVICE, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO, +) + +DOMAIN: Final = "ambee" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=180) + +SERVICE_AIR_QUALITY: Final = ("air_quality", "Air Quality") + +SENSORS: dict[str, dict[str, Any]] = { + "particulate_matter_2_5": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Particulate Matter < 2.5 μm", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "particulate_matter_10": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Particulate Matter < 10 μm", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "sulphur_dioxide": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Sulphur Dioxide (SO2)", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "nitrogen_dioxide": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Nitrogen Dioxide (NO2)", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "ozone": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Ozone", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "carbon_monoxide": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Carbon Monoxide (CO)", + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "air_quality_index": { + ATTR_SERVICE: SERVICE_AIR_QUALITY, + ATTR_NAME: "Air Quality Index (AQI)", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, +} diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json new file mode 100644 index 00000000000..f3f3a0f9f49 --- /dev/null +++ b/homeassistant/components/ambee/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ambee", + "name": "Ambee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ambee", + "requirements": ["ambee==0.2.1"], + "codeowners": ["@frenck"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py new file mode 100644 index 00000000000..0bb626afb62 --- /dev/null +++ b/homeassistant/components/ambee/sensor.py @@ -0,0 +1,69 @@ +"""Support for Ambee sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_NAME, + ATTR_SERVICE, + ATTR_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, SENSORS + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Ambee sensor based on a config entry.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AmbeeSensor(coordinator=coordinator, entry_id=entry.entry_id, key=sensor) + for sensor in SENSORS + ) + + +class AmbeeSensor(CoordinatorEntity, SensorEntity): + """Defines an Ambee sensor.""" + + def __init__( + self, *, coordinator: DataUpdateCoordinator, entry_id: str, key: str + ) -> None: + """Initialize Ambee sensor.""" + super().__init__(coordinator=coordinator) + self._key = key + self._entry_id = entry_id + self._service_key, self._service_name = SENSORS[key][ATTR_SERVICE] + + self._attr_device_class = SENSORS[key].get(ATTR_DEVICE_CLASS) + self._attr_name = SENSORS[key][ATTR_NAME] + self._attr_state_class = SENSORS[key].get(ATTR_STATE_CLASS) + self._attr_unique_id = f"{entry_id}_{key}" + self._attr_unit_of_measurement = SENSORS[key].get(ATTR_UNIT_OF_MEASUREMENT) + + @property + def state(self) -> StateType: + """Return the state of the sensor.""" + return getattr(self.coordinator.data, self._key) # type: ignore[no-any-return] + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Ambee Service.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, f"{self._entry_id}_{self._service_key}")}, + ATTR_NAME: self._service_name, + ATTR_MANUFACTURER: "Ambee", + } diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json new file mode 100644 index 00000000000..8bec71ebe29 --- /dev/null +++ b/homeassistant/components/ambee/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up Ambee to integrate with Home Assistant.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "name": "[%key:common::config_flow::data::name%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + } + } +} diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json new file mode 100644 index 00000000000..8728f56fb4e --- /dev/null +++ b/homeassistant/components/ambee/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "Set up Ambee to integrate with Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b887574c055..442d6e9be08 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -18,6 +18,7 @@ FLOWS = [ "airvisual", "alarmdecoder", "almond", + "ambee", "ambiclimate", "ambient_station", "apple_tv", diff --git a/mypy.ini b/mypy.ini index c25dc4ec1f6..d501056ecec 100644 --- a/mypy.ini +++ b/mypy.ini @@ -143,6 +143,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ambee.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ampio.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 6b554284c87..f7211e3509a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -253,6 +253,9 @@ aladdin_connect==0.3 # homeassistant.components.alpha_vantage alpha_vantage==2.3.1 +# homeassistant.components.ambee +ambee==0.2.1 + # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eaf2cf98fb0..930965768fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,9 @@ aioymaps==1.1.0 # homeassistant.components.airly airly==1.1.0 +# homeassistant.components.ambee +ambee==0.2.1 + # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/tests/components/ambee/__init__.py b/tests/components/ambee/__init__.py new file mode 100644 index 00000000000..94c88557803 --- /dev/null +++ b/tests/components/ambee/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ambee integration.""" diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py new file mode 100644 index 00000000000..1f91a842321 --- /dev/null +++ b/tests/components/ambee/test_config_flow.py @@ -0,0 +1,129 @@ +"""Tests for the Ambee config flow.""" + +from unittest.mock import patch + +from ambee import AmbeeAuthenticationError, AmbeeError + +from homeassistant.components.ambee.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality" + ) as mock_ambee, patch( + "homeassistant.components.ambee.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_API_KEY: "example", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Name" + assert result2.get("data") == { + CONF_API_KEY: "example", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_ambee.mock_calls) == 1 + + +async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: + """Test the full user configuration flow with an authentication error. + + This tests tests a full config flow, with a case the user enters an invalid + API token, but recover by entering the correct one. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality", + side_effect=AmbeeAuthenticationError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_API_KEY: "invalid", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "invalid_api_key"} + assert "flow_id" in result2 + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality" + ) as mock_ambee, patch( + "homeassistant.components.ambee.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_API_KEY: "example", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Name" + assert result3.get("data") == { + CONF_API_KEY: "example", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_ambee.mock_calls) == 1 + + +async def test_api_error(hass: HomeAssistant) -> None: + """Test API error.""" + with patch( + "homeassistant.components.ambee.Ambee.air_quality", + side_effect=AmbeeError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_NAME: "Name", + CONF_API_KEY: "example", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + }, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {"base": "cannot_connect"} From a6a34c76f777cbf5b8328b57f58bfa6dffa9bb68 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Jun 2021 13:31:31 +0200 Subject: [PATCH 263/750] Add color mode support to WLED (#51648) * Add color mode support to WLED * Update homeassistant/components/wled/light.py Co-authored-by: Erik Montnemery * Update homeassistant/components/wled/light.py Co-authored-by: Erik Montnemery * Update homeassistant/components/wled/light.py Co-authored-by: Erik Montnemery * Update homeassistant/components/wled/light.py Co-authored-by: Erik Montnemery * black * property, property Co-authored-by: Erik Montnemery --- homeassistant/components/wled/light.py | 99 +++++++++----------------- tests/components/wled/test_light.py | 53 ++------------ 2 files changed, 36 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index d9240e75efb..c40c61f98f4 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -2,23 +2,21 @@ from __future__ import annotations from functools import partial -from typing import Any +from typing import Any, Tuple, cast import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, SUPPORT_EFFECT, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -28,7 +26,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -import homeassistant.util.color as color_util from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( @@ -96,14 +93,16 @@ async def async_setup_entry( class WLEDMasterLight(WLEDEntity, LightEntity): """Defines a WLED master light.""" - _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_color_mode = COLOR_MODE_BRIGHTNESS _attr_icon = "mdi:led-strip-variant" + _attr_supported_features = SUPPORT_TRANSITION def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" super().__init__(coordinator=coordinator) self._attr_name = f"{coordinator.data.info.name} Master" self._attr_unique_id = coordinator.data.info.mac_address + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} @property def brightness(self) -> int | None: @@ -165,12 +164,14 @@ class WLEDMasterLight(WLEDEntity, LightEntity): class WLEDSegmentLight(WLEDEntity, LightEntity): """Defines a WLED light based on a segment.""" + _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION _attr_icon = "mdi:led-strip-variant" def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: """Initialize WLED segment light.""" super().__init__(coordinator=coordinator) self._rgbw = coordinator.data.info.leds.rgbw + self._wv = coordinator.data.info.leds.wv self._segment = segment # If this is the one and only segment, use a simpler name @@ -182,6 +183,12 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): f"{self.coordinator.data.info.mac_address}_{self._segment}" ) + self._attr_color_mode = COLOR_MODE_RGB + self._attr_supported_color_modes = {COLOR_MODE_RGB} + if self._rgbw and self._wv: + self._attr_color_mode = COLOR_MODE_RGBW + self._attr_supported_color_modes = {COLOR_MODE_RGBW} + @property def available(self) -> bool: """Return True if entity is available.""" @@ -214,10 +221,17 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): } @property - def hs_color(self) -> tuple[float, float]: - """Return the hue and saturation color value [float, float].""" - color = self.coordinator.data.state.segments[self._segment].color_primary - return color_util.color_RGB_to_hs(*color[:3]) + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the color value.""" + return self.coordinator.data.state.segments[self._segment].color_primary[:3] + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the color value.""" + return cast( + Tuple[int, int, int, int], + self.coordinator.data.state.segments[self._segment].color_primary, + ) @property def effect(self) -> str | None: @@ -238,28 +252,6 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): return state.segments[self._segment].brightness - @property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - color = self.coordinator.data.state.segments[self._segment].color_primary - return color[-1] if self._rgbw else None - - @property - def supported_features(self) -> int: - """Flag supported features.""" - flags = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - ) - - if self._rgbw: - flags |= SUPPORT_WHITE_VALUE - - return flags - @property def effect_list(self) -> list[str]: """Return the list of supported effects.""" @@ -301,17 +293,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): ATTR_SEGMENT_ID: self._segment, } - if ATTR_COLOR_TEMP in kwargs: - mireds = color_util.color_temperature_kelvin_to_mired( - kwargs[ATTR_COLOR_TEMP] - ) - data[ATTR_COLOR_PRIMARY] = tuple( - map(int, color_util.color_temperature_to_rgb(mireds)) - ) + if ATTR_RGB_COLOR in kwargs: + data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGB_COLOR] - if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs[ATTR_HS_COLOR] - data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + if ATTR_RGBW_COLOR in kwargs: + data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGBW_COLOR] if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -323,27 +309,6 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if ATTR_EFFECT in kwargs: data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] - # Support for RGBW strips, adds white value - if self._rgbw and any( - x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE) for x in kwargs - ): - # WLED cannot just accept a white value, it needs the color. - # We use the last know color in case just the white value changes. - if all(x not in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): - hue, sat = self.hs_color - data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) - - # On a RGBW strip, when the color is pure white, disable the RGB LEDs in - # WLED by setting RGB to 0,0,0 - if data[ATTR_COLOR_PRIMARY] == (255, 255, 255): - data[ATTR_COLOR_PRIMARY] = (0, 0, 0) - - # Add requested or last known white value - if ATTR_WHITE_VALUE in kwargs: - data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) - else: - data[ATTR_COLOR_PRIMARY] += (self.white_value,) - # When only 1 segment is present, switch along the master, and use # the master for power/brightness control. if len(self.coordinator.data.state.segments) == 1: diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 0077cea0202..268c527a763 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -6,12 +6,11 @@ from wled import Device as WLEDDevice, WLEDConnectionError from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.wled import SCAN_INTERVAL @@ -144,20 +143,6 @@ async def test_segment_change_state( transition=50, ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_COLOR_TEMP: 400}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - color_primary=(255, 159, 70), - on=True, - segment_id=0, - ) - async def test_master_change_state( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog @@ -394,36 +379,7 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) - assert state.attributes.get(ATTR_WHITE_VALUE) == 139 - - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - on=True, - segment_id=0, - color_primary=(255, 159, 70, 139), - ) - - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - color_primary=(255, 0, 0, 100), - on=True, - segment_id=0, - ) + assert state.attributes.get(ATTR_RGBW_COLOR) == (255, 0, 0, 139) with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( @@ -431,14 +387,13 @@ async def test_rgbw_light( SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.wled_rgbw_light", - ATTR_RGB_COLOR: (255, 255, 255), - ATTR_WHITE_VALUE: 100, + ATTR_RGBW_COLOR: (255, 255, 255, 255), }, blocking=True, ) await hass.async_block_till_done() light_mock.assert_called_once_with( - color_primary=(0, 0, 0, 100), + color_primary=(255, 255, 255, 255), on=True, segment_id=0, ) From c21895facb89667136fd0ed331e809882eb7ca27 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 9 Jun 2021 13:33:41 +0200 Subject: [PATCH 264/750] Remove ASUS.gpio / not working with new GCC (#51662) --- machine/tinker | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/machine/tinker b/machine/tinker index 20efbe964e8..04a0aa6dc2c 100644 --- a/machine/tinker +++ b/machine/tinker @@ -7,14 +7,3 @@ RUN apk --no-cache add usbutils \ bluepy \ pybluez \ pygatt[GATTTOOL] - -# Install GPIO support -RUN apk add --no-cache --virtual .build-dependencies \ - gcc libc-dev musl-dev \ - && git clone --depth 1 https://github.com/TinkerBoard/gpio_lib_python /usr/src/gpio \ - && cd /usr/src/gpio \ - && sed -i "s/caddr_t/void*/g" source/wiringTB.c \ - && export MAKEFLAGS="-j$(nproc)" \ - && python3 setup.py install \ - && apk del .build-dependencies \ - && rm -rf /usr/src/gpio From d3a4e21cb5731103bf4d6a1587b2fa190400e11c Mon Sep 17 00:00:00 2001 From: rianadon Date: Wed, 9 Jun 2021 05:06:24 -0700 Subject: [PATCH 265/750] Convert ecobee pressure to local units (#51379) * Convert ecobee pressure to local units * Round inHg to 2 places --- homeassistant/components/ecobee/weather.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 7774a6648a5..fc93ebffb95 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -12,8 +12,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT from homeassistant.util import dt as dt_util +from homeassistant.util.pressure import convert as pressure_convert from .const import ( _LOGGER, @@ -113,7 +114,11 @@ class EcobeeWeather(WeatherEntity): def pressure(self): """Return the pressure.""" try: - return int(self.get_forecast(0, "pressure")) + pressure = self.get_forecast(0, "pressure") + if not self.hass.config.units.is_metric: + pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) + return round(pressure, 2) + return round(pressure) except ValueError: return None From 6e20edc30c96966da49314523fb4b5e36dd222c0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 9 Jun 2021 14:08:29 +0200 Subject: [PATCH 266/750] Update xknx to version 0.18.5 (#51644) * xknx 0.18.5 * fix integer DPTs trying to cast str state with `int()` * Delint Co-authored-by: Martin Hjelmare --- homeassistant/components/knx/expose.py | 14 +++++++++++--- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5c371445cc4..5b57e2b0b4c 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -5,6 +5,8 @@ from typing import Callable from xknx import XKNX from xknx.devices import DateTime, ExposeSensor +from xknx.dpt import DPTNumeric +from xknx.remote_value import RemoteValueSensor from homeassistant.const import ( CONF_ENTITY_ID, @@ -122,9 +124,15 @@ class KNXExposeSensor: ) if self.type == "binary": if value in (1, STATE_ON, "True"): - value = True - elif value in (0, STATE_OFF, "False"): - value = False + return True + if value in (0, STATE_OFF, "False"): + return False + if ( + value is not None + and isinstance(self.device.sensor_value, RemoteValueSensor) + and issubclass(self.device.sensor_value.dpt_class, DPTNumeric) + ): + return float(value) return value async def _async_entity_changed(self, event: Event) -> None: diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 6b1d4d328ac..65a74a72518 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.4"], + "requirements": ["xknx==0.18.5"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f7211e3509a..b85c396af79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2377,7 +2377,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.4 +xknx==0.18.5 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 930965768fe..27e6192a968 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1286,7 +1286,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.4 +xknx==0.18.5 # homeassistant.components.bluesound # homeassistant.components.rest From e78c656bfe3cad6a3ca50d2a6d10cb2bcc0c0b2f Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Wed, 9 Jun 2021 15:30:33 +0300 Subject: [PATCH 267/750] Static typing for Uptime (#51638) * uptime typing * Clean up name type Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + homeassistant/components/uptime/sensor.py | 29 +++++++++++++++-------- mypy.ini | 11 +++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index b44b04dd606..9a25425d39e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -71,6 +71,7 @@ homeassistant.components.systemmonitor.* homeassistant.components.tcp.* homeassistant.components.tts.* homeassistant.components.upcloud.* +homeassistant.components.uptime.* homeassistant.components.vacuum.* homeassistant.components.water_heater.* homeassistant.components.weather.* diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 7e79c2fbb5e..98c673b8878 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -1,14 +1,18 @@ """Platform to retrieve uptime for Home Assistant.""" +from __future__ import annotations import voluptuous as vol -from homeassistant.components.sensor import ( +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TIMESTAMP, - PLATFORM_SCHEMA, - SensorEntity, ) -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util DEFAULT_NAME = "Uptime" @@ -26,9 +30,14 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the uptime sensor platform.""" - name = config.get(CONF_NAME) + name = config[CONF_NAME] async_add_entities([UptimeSensor(name)], True) @@ -36,23 +45,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize the uptime sensor.""" self._name = name self._state = dt_util.now().isoformat() @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def device_class(self): + def device_class(self) -> str: """Return device class.""" return DEVICE_CLASS_TIMESTAMP @property - def state(self): + def state(self) -> str: """Return the state of the sensor.""" return self._state diff --git a/mypy.ini b/mypy.ini index d501056ecec..89a4b6fc122 100644 --- a/mypy.ini +++ b/mypy.ini @@ -792,6 +792,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.uptime.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.vacuum.*] check_untyped_defs = true disallow_incomplete_defs = true From cdf256b93e3f72f94a81a311383092978a393db9 Mon Sep 17 00:00:00 2001 From: Kenny Millington Date: Wed, 9 Jun 2021 17:16:48 +0100 Subject: [PATCH 268/750] Create docker series version tag YYYY.M (#51615) * Create docker series version tag YYYY.M.x * Only create docker series version for stable tags Following review on the PR51615 * Remove the ".x" suffix for docker series tags Following review on PR51615 * Fix the in-line comment Oversight in previous commit --- .github/workflows/builder.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 607af99fb51..e62bb19af49 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -307,5 +307,9 @@ jobs: create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + + # Create series version tag (e.g. 2021.6) + v="${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}" fi done From c512e1df3c9986ce3f2c00aa5d1f0573a268fd2a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 9 Jun 2021 11:20:28 -0500 Subject: [PATCH 269/750] Bump pysonos to 0.0.51 (#51669) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index bb949fea8c1..a3b031ac07b 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.50"], + "requirements": ["pysonos==0.0.51"], "dependencies": ["ssdp"], "after_dependencies": ["plex"], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index b85c396af79..b8888342fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1761,7 +1761,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.50 +pysonos==0.0.51 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27e6192a968..10ee6c57164 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.50 +pysonos==0.0.51 # homeassistant.components.spc pyspcwebgw==0.4.0 From 332c86ff8c9600eb8b5bdacbe49a0ba0695e56b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Jun 2021 20:15:46 +0200 Subject: [PATCH 270/750] Restructure WLED integration (#51667) --- homeassistant/components/wled/__init__.py | 105 +------------------ homeassistant/components/wled/const.py | 5 + homeassistant/components/wled/coordinator.py | 41 ++++++++ homeassistant/components/wled/helpers.py | 28 +++++ homeassistant/components/wled/light.py | 4 +- homeassistant/components/wled/models.py | 30 ++++++ homeassistant/components/wled/sensor.py | 3 +- homeassistant/components/wled/switch.py | 4 +- tests/components/wled/test_config_flow.py | 15 ++- tests/components/wled/test_init.py | 5 +- tests/components/wled/test_light.py | 21 ++-- tests/components/wled/test_switch.py | 7 +- 12 files changed, 145 insertions(+), 123 deletions(-) create mode 100644 homeassistant/components/wled/coordinator.py create mode 100644 homeassistant/components/wled/helpers.py create mode 100644 homeassistant/components/wled/models.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index b44df82f889..bd8316a2ff0 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,44 +1,21 @@ """Support for WLED.""" from __future__ import annotations -from datetime import timedelta -import logging - -from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError - from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, - CONF_HOST, -) +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator -SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WLED from a config entry.""" - - # Create WLED instance for this entry coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) await coordinator.async_config_entry_first_refresh() @@ -59,85 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WLED config entry.""" - - # Unload entities for this entry/device. unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] - - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - return unload_ok - - -def wled_exception_handler(func): - """Decorate WLED calls to handle WLED exceptions. - - A decorator that wraps the passed in function, catches WLED errors, - and handles the availability of the device in the data coordinator. - """ - - async def handler(self, *args, **kwargs): - try: - await func(self, *args, **kwargs) - self.coordinator.update_listeners() - - except WLEDConnectionError as error: - _LOGGER.error("Error communicating with API: %s", error) - self.coordinator.last_update_success = False - self.coordinator.update_listeners() - - except WLEDError as error: - _LOGGER.error("Invalid response from API: %s", error) - - return handler - - -class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): - """Class to manage fetching WLED data from single endpoint.""" - - def __init__( - self, - hass: HomeAssistant, - *, - host: str, - ) -> None: - """Initialize global WLED data updater.""" - self.wled = WLED(host, session=async_get_clientsession(hass)) - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - ) - - def update_listeners(self) -> None: - """Call update on all listeners.""" - for update_callback in self._listeners: - update_callback() - - async def _async_update_data(self) -> WLEDDevice: - """Fetch data from WLED.""" - try: - return await self.wled.update(full_update=not self.last_update_success) - except WLEDError as error: - raise UpdateFailed(f"Invalid response from API: {error}") from error - - -class WLEDEntity(CoordinatorEntity): - """Defines a base WLED entity.""" - - coordinator: WLEDDataUpdateCoordinator - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this WLED device.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, - ATTR_NAME: self.coordinator.data.info.name, - ATTR_MANUFACTURER: self.coordinator.data.info.brand, - ATTR_MODEL: self.coordinator.data.info.product, - ATTR_SW_VERSION: self.coordinator.data.info.version, - } diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 7cc52601d79..8f759ea3e90 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -1,8 +1,13 @@ """Constants for the WLED integration.""" +from datetime import timedelta +import logging # Integration domain DOMAIN = "wled" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(seconds=5) + # Attributes ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py new file mode 100644 index 00000000000..8a9312dfc0a --- /dev/null +++ b/homeassistant/components/wled/coordinator.py @@ -0,0 +1,41 @@ +"""DataUpdateCoordinator for WLED.""" + +from wled import WLED, Device as WLEDDevice, WLEDError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + + +class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): + """Class to manage fetching WLED data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + *, + host: str, + ) -> None: + """Initialize global WLED data updater.""" + self.wled = WLED(host, session=async_get_clientsession(hass)) + + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def update_listeners(self) -> None: + """Call update on all listeners.""" + for update_callback in self._listeners: + update_callback() + + async def _async_update_data(self) -> WLEDDevice: + """Fetch data from WLED.""" + try: + return await self.wled.update(full_update=not self.last_update_success) + except WLEDError as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py new file mode 100644 index 00000000000..d5ca895390b --- /dev/null +++ b/homeassistant/components/wled/helpers.py @@ -0,0 +1,28 @@ +"""Helpers for WLED.""" + +from wled import WLEDConnectionError, WLEDError + +from .const import LOGGER + + +def wled_exception_handler(func): + """Decorate WLED calls to handle WLED exceptions. + + A decorator that wraps the passed in function, catches WLED errors, + and handles the availability of the device in the data coordinator. + """ + + async def handler(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + self.coordinator.update_listeners() + + except WLEDConnectionError as error: + LOGGER.error("Error communicating with API: %s", error) + self.coordinator.last_update_success = False + self.coordinator.update_listeners() + + except WLEDError as error: + LOGGER.error("Invalid response from API: %s", error) + + return handler diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index c40c61f98f4..ed1ec22a65c 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -27,7 +27,6 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_COLOR_PRIMARY, ATTR_INTENSITY, @@ -42,6 +41,9 @@ from .const import ( SERVICE_EFFECT, SERVICE_PRESET, ) +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py new file mode 100644 index 00000000000..de8628fc755 --- /dev/null +++ b/homeassistant/components/wled/models.py @@ -0,0 +1,30 @@ +"""Models for WLED.""" +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator + + +class WLEDEntity(CoordinatorEntity): + """Defines a base WLED entity.""" + + coordinator: WLEDDataUpdateCoordinator + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this WLED device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, + ATTR_NAME: self.coordinator.data.info.name, + ATTR_MANUFACTURER: self.coordinator.data.info.brand, + ATTR_MODEL: self.coordinator.data.info.product, + ATTR_SW_VERSION: self.coordinator.data.info.version, + } diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index f6b9b0d973a..4008c42f292 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -17,8 +17,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import WLEDDataUpdateCoordinator, WLEDEntity from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .models import WLEDEntity async def async_setup_entry( diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 2d1801a0c5e..74f58472a19 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_DURATION, ATTR_FADE, @@ -16,6 +15,9 @@ from .const import ( ATTR_UDP_PORT, DOMAIN, ) +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity PARALLEL_UPDATES = 1 diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index e828c632451..358902a8821 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -91,7 +91,10 @@ async def test_full_zeroconf_flow_implementation( assert result2["data"][CONF_MAC] == "aabbccddeeff" -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +@patch( + "homeassistant.components.wled.coordinator.WLED.update", + side_effect=WLEDConnectionError, +) async def test_connection_error( update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: @@ -109,7 +112,10 @@ async def test_connection_error( assert result.get("errors") == {"base": "cannot_connect"} -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +@patch( + "homeassistant.components.wled.coordinator.WLED.update", + side_effect=WLEDConnectionError, +) async def test_zeroconf_connection_error( update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: @@ -126,7 +132,10 @@ async def test_zeroconf_connection_error( assert result.get("reason") == "cannot_connect" -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +@patch( + "homeassistant.components.wled.coordinator.WLED.update", + side_effect=WLEDConnectionError, +) async def test_zeroconf_confirm_connection_error( update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 8db7e266e80..52b014760e5 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -11,7 +11,10 @@ from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +@patch( + "homeassistant.components.wled.coordinator.WLED.update", + side_effect=WLEDConnectionError, +) async def test_config_entry_not_ready( mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 268c527a763..721c0cc3880 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -13,7 +13,6 @@ from homeassistant.components.light import ( ATTR_TRANSITION, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.wled import SCAN_INTERVAL from homeassistant.components.wled.const import ( ATTR_INTENSITY, ATTR_PALETTE, @@ -22,6 +21,7 @@ from homeassistant.components.wled.const import ( ATTR_REVERSE, ATTR_SPEED, DOMAIN, + SCAN_INTERVAL, SERVICE_EFFECT, SERVICE_PRESET, ) @@ -228,7 +228,7 @@ async def test_dynamically_handle_segments( # Test removal if segment went missing, including the master entity with patch( - "homeassistant.components.wled.WLED.update", + "homeassistant.components.wled.coordinator.WLED.update", return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) @@ -257,7 +257,7 @@ async def test_single_segment_behavior( # Test absent master with patch( - "homeassistant.components.wled.WLED.update", + "homeassistant.components.wled.coordinator.WLED.update", return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) @@ -273,7 +273,7 @@ async def test_single_segment_behavior( device.state.brightness = 100 device.state.segments[0].brightness = 255 with patch( - "homeassistant.components.wled.WLED.update", + "homeassistant.components.wled.coordinator.WLED.update", return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) @@ -286,7 +286,7 @@ async def test_single_segment_behavior( # Test segment is off when master is off device.state.on = False with patch( - "homeassistant.components.wled.WLED.update", + "homeassistant.components.wled.coordinator.WLED.update", return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) @@ -336,7 +336,7 @@ async def test_light_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"): + with patch("homeassistant.components.wled.coordinator.WLED.update"): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -356,8 +356,9 @@ async def test_light_connection_error( """Test error handling of the WLED switches.""" await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"), patch( - "homeassistant.components.wled.WLED.segment", side_effect=WLEDConnectionError + with patch("homeassistant.components.wled.coordinator.WLED.update"), patch( + "homeassistant.components.wled.coordinator.WLED.segment", + side_effect=WLEDConnectionError, ): await hass.services.async_call( LIGHT_DOMAIN, @@ -532,7 +533,7 @@ async def test_effect_service_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"): + with patch("homeassistant.components.wled.coordinator.WLED.update"): await hass.services.async_call( DOMAIN, SERVICE_EFFECT, @@ -575,7 +576,7 @@ async def test_preset_service_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"): + with patch("homeassistant.components.wled.coordinator.WLED.update"): await hass.services.async_call( DOMAIN, SERVICE_PRESET, diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index ddeeee41ac8..7a396092ff9 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -144,7 +144,7 @@ async def test_switch_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"): + with patch("homeassistant.components.wled.coordinator.WLED.update"): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -164,8 +164,9 @@ async def test_switch_connection_error( """Test error handling of the WLED switches.""" await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLED.update"), patch( - "homeassistant.components.wled.WLED.nightlight", side_effect=WLEDConnectionError + with patch("homeassistant.components.wled.coordinator.WLED.update"), patch( + "homeassistant.components.wled.coordinator.WLED.nightlight", + side_effect=WLEDConnectionError, ): await hass.services.async_call( SWITCH_DOMAIN, From e5c6ac5ba8ed21dc42a1684ffbe7e83db68a6618 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Jun 2021 20:23:16 +0200 Subject: [PATCH 271/750] Add 100% test coverage to Ambee integration (#51670) * Add 100% test coverage to Ambee integration * Add tests for device and entity registry --- .coveragerc | 2 - tests/components/ambee/conftest.py | 50 ++++++++++ tests/components/ambee/test_init.py | 46 +++++++++ tests/components/ambee/test_sensor.py | 130 ++++++++++++++++++++++++++ tests/fixtures/ambee/air_quality.json | 28 ++++++ 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 tests/components/ambee/conftest.py create mode 100644 tests/components/ambee/test_init.py create mode 100644 tests/components/ambee/test_sensor.py create mode 100644 tests/fixtures/ambee/air_quality.json diff --git a/.coveragerc b/.coveragerc index 1d2c6275fc3..9437f8943a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -45,8 +45,6 @@ omit = homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/* - homeassistant/components/ambee/__init__.py - homeassistant/components/ambee/sensor.py homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/* homeassistant/components/amcrest/* diff --git a/tests/components/ambee/conftest.py b/tests/components/ambee/conftest.py new file mode 100644 index 00000000000..de88e28e1d1 --- /dev/null +++ b/tests/components/ambee/conftest.py @@ -0,0 +1,50 @@ +"""Fixtures for Ambee integration tests.""" +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from ambee import AirQuality +import pytest + +from homeassistant.components.ambee.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Home Sweet Home", + domain=DOMAIN, + data={CONF_LATITUDE: 52.42, CONF_LONGITUDE: 4.44, CONF_API_KEY: "example"}, + unique_id="unique_thingy", + ) + + +@pytest.fixture +def mock_ambee(aioclient_mock: AiohttpClientMocker): + """Return a mocked Ambee client.""" + with patch("homeassistant.components.ambee.Ambee") as ambee_mock: + client = ambee_mock.return_value + client.air_quality = AsyncMock( + return_value=AirQuality.from_dict( + json.loads(load_fixture("ambee/air_quality.json")) + ) + ) + yield ambee_mock + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: MagicMock +) -> MockConfigEntry: + """Set up the Ambee integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py new file mode 100644 index 00000000000..c58e7cfef0d --- /dev/null +++ b/tests/components/ambee/test_init.py @@ -0,0 +1,46 @@ +"""Tests for the Ambee integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from ambee import AmbeeConnectionError + +from homeassistant.components.ambee.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ambee: AsyncMock, +) -> None: + """Test the Ambee configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + + +@patch( + "homeassistant.components.ambee.Ambee.air_quality", + side_effect=AmbeeConnectionError, +) +async def test_config_entry_not_ready( + mock_air_quality: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Ambee configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_air_quality.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py new file mode 100644 index 00000000000..a754256ff0a --- /dev/null +++ b/tests/components/ambee/test_sensor.py @@ -0,0 +1,130 @@ +"""Tests for the sensors provided by the Ambee integration.""" +from homeassistant.components.ambee.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_air_quality( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Ambee Air Quality sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.particulate_matter_2_5_mm") + entry = entity_registry.async_get("sensor.particulate_matter_2_5_mm") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_particulate_matter_2_5" + assert state.state == "3.14" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.particulate_matter_10_mm") + entry = entity_registry.async_get("sensor.particulate_matter_10_mm") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_particulate_matter_10" + assert state.state == "5.24" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.sulphur_dioxide_so2") + entry = entity_registry.async_get("sensor.sulphur_dioxide_so2") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_sulphur_dioxide" + assert state.state == "0.031" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_BILLION + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.nitrogen_dioxide_no2") + entry = entity_registry.async_get("sensor.nitrogen_dioxide_no2") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_nitrogen_dioxide" + assert state.state == "0.66" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_BILLION + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.ozone") + entry = entity_registry.async_get("sensor.ozone") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_ozone" + assert state.state == "17.067" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_BILLION + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.carbon_monoxide_co") + entry = entity_registry.async_get("sensor.carbon_monoxide_co") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_carbon_monoxide" + assert state.state == "0.105" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_MILLION + ) + + state = hass.states.get("sensor.air_quality_index_aqi") + entry = entity_registry.async_get("sensor.air_quality_index_aqi") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_air_quality_index" + assert state.state == "13" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} + assert device_entry.manufacturer == "Ambee" + assert device_entry.name == "Air Quality" + assert not device_entry.model + assert not device_entry.sw_version diff --git a/tests/fixtures/ambee/air_quality.json b/tests/fixtures/ambee/air_quality.json new file mode 100644 index 00000000000..2844e38168b --- /dev/null +++ b/tests/fixtures/ambee/air_quality.json @@ -0,0 +1,28 @@ +{ + "message": "success", + "stations": [ + { + "CO": 0.105, + "NO2": 0.66, + "OZONE": 17.067, + "PM10": 5.24, + "PM25": 3.14, + "SO2": 0.031, + "city": "Hellendoorn", + "countryCode": "NL", + "division": "", + "lat": 52.3981, + "lng": 6.4493, + "placeName": "Hellendoorn", + "postalCode": "7447", + "state": "Overijssel", + "updatedAt": "2021-05-29T14:00:00.000Z", + "AQI": 13, + "aqiInfo": { + "pollutant": "PM2.5", + "concentration": 3.14, + "category": "Good" + } + } + ] +} From 417ba5538db4be304bf2cd0a50cf037d7f5d28b1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Jun 2021 00:11:17 +0000 Subject: [PATCH 272/750] [ci skip] Translation update --- .../components/ambee/translations/et.json | 19 +++++++++++++ .../components/ambee/translations/ru.json | 19 +++++++++++++ .../ambee/translations/zh-Hant.json | 19 +++++++++++++ .../modern_forms/translations/zh-Hant.json | 28 +++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 homeassistant/components/ambee/translations/et.json create mode 100644 homeassistant/components/ambee/translations/ru.json create mode 100644 homeassistant/components/ambee/translations/zh-Hant.json create mode 100644 homeassistant/components/modern_forms/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json new file mode 100644 index 00000000000..e6ec7654a64 --- /dev/null +++ b/homeassistant/components/ambee/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_api_key": "Vale API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Seadista Ambee sidumine Home Assistantiga." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json new file mode 100644 index 00000000000..dc585336313 --- /dev/null +++ b/homeassistant/components/ambee/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "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 Ambee." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json new file mode 100644 index 00000000000..b4b37b25102 --- /dev/null +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/zh-Hant.json b/homeassistant/components/modern_forms/translations/zh-Hant.json new file mode 100644 index 00000000000..df3ebf486c7 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a Modern Forms \u98a8\u6247\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e\u540d\u7a31 `{name}` Modern Forms \u98a8\u6247\u81f3 Home Assistant\uff1f", + "title": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Modern Forms \u98a8\u6247\u88dd\u7f6e" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file From c362ffd384fba2820b8eb3c1f3ed7d13a4ee47cf Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 9 Jun 2021 23:31:14 -0500 Subject: [PATCH 273/750] Clean up unused Sonos subscriptions (#51583) --- homeassistant/components/sonos/__init__.py | 4 ++++ homeassistant/components/sonos/entity.py | 5 +++-- homeassistant/components/sonos/speaker.py | 18 ++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 4bb9b475b9a..a20805ce136 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -129,6 +129,10 @@ async def async_setup_entry( # noqa: C901 pysonos.config.EVENT_ADVERTISE_IP = advertise_addr async def _async_stop_event_listener(event: Event) -> None: + await asyncio.gather( + *[speaker.async_unsubscribe() for speaker in data.discovered.values()], + return_exceptions=True, + ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 7d4e168c960..290c6a64cb1 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -64,13 +64,14 @@ class SonosEntity(Entity): async def async_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" - if self.speaker.is_first_poll: + if not self.speaker.subscriptions_failed: _LOGGER.warning( "%s cannot reach [%s], falling back to polling, functionality may be limited", self.speaker.zone_name, self.speaker.subscription_address, ) - self.speaker.is_first_poll = False + self.speaker.subscriptions_failed = True + await self.speaker.async_unsubscribe() try: await self.async_update() # pylint: disable=no-member except (OSError, SoCoException) as ex: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 47c9af62761..2ddd7148478 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -149,11 +149,11 @@ class SonosSpeaker: self.media = SonosMedia(soco) # Synchronization helpers - self.is_first_poll: bool = True self._is_ready: bool = False self._platforms_ready: set[str] = set() # Subscriptions and events + self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] self._resubscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} @@ -331,6 +331,15 @@ class SonosSpeaker: subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) + async def async_unsubscribe(self) -> None: + """Cancel all subscriptions.""" + _LOGGER.debug("Unsubscribing from events for %s", self.zone_name) + await asyncio.gather( + *[subscription.unsubscribe() for subscription in self._subscriptions], + return_exceptions=True, + ) + self._subscriptions = [] + @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" @@ -445,7 +454,7 @@ class SonosSpeaker: SCAN_INTERVAL, ) - if self._is_ready: + if self._is_ready and not self.subscriptions_failed: done = await self.async_subscribe() if not done: assert self._seen_timer is not None @@ -466,10 +475,7 @@ class SonosSpeaker: self._poll_timer() self._poll_timer = None - for subscription in self._subscriptions: - await subscription.unsubscribe() - - self._subscriptions = [] + await self.async_unsubscribe() if not will_reconnect: self.hass.data[DATA_SONOS].ssdp_known.remove(self.soco.uid) From b165e9f0cff3e2d7d7c28ae21299379974d4282e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Jun 2021 08:16:25 +0200 Subject: [PATCH 274/750] Upgrade ambee to 0.3.0 (#51676) --- homeassistant/components/ambee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index f3f3a0f9f49..7293f8ea4b4 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -3,7 +3,7 @@ "name": "Ambee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", - "requirements": ["ambee==0.2.1"], + "requirements": ["ambee==0.3.0"], "codeowners": ["@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b8888342fc5..7bfb16f4111 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -254,7 +254,7 @@ aladdin_connect==0.3 alpha_vantage==2.3.1 # homeassistant.components.ambee -ambee==0.2.1 +ambee==0.3.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10ee6c57164..067b35c72ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioymaps==1.1.0 airly==1.1.0 # homeassistant.components.ambee -ambee==0.2.1 +ambee==0.3.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 9097f412195dae83060d2b7c3670c7f240c07fc2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Jun 2021 08:18:59 +0200 Subject: [PATCH 275/750] Correct comment in MQTT fan (#51682) --- homeassistant/components/mqtt/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 5cd924551f7..d92b6dfd21f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -124,14 +124,14 @@ def valid_preset_mode_configuration(config): PLATFORM_SCHEMA = vol.All( - # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and + # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, # are deprecated, support will be removed after a quarter (2021.7) cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), cv.deprecated(CONF_PAYLOAD_LOW_SPEED), cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), - cv.deprecated(CONF_SPEED_LIST), cv.deprecated(CONF_SPEED_COMMAND_TOPIC), + cv.deprecated(CONF_SPEED_LIST), cv.deprecated(CONF_SPEED_STATE_TOPIC), cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( From b1022ce84e6ff06e9b94e5b59985cb619019dc36 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Jun 2021 08:51:58 +0200 Subject: [PATCH 276/750] Use supported color modes in deCONZ integration (#51656) * Initial commit everything is working, need to reevaluate tests * Fix supported color modes and hs_color * Attest color mode --- homeassistant/components/deconz/light.py | 101 ++++++++++++++++------- tests/components/deconz/test_light.py | 28 +++++-- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 60aa02c153e..adcfb324ebe 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,5 +1,8 @@ """Support for deCONZ lights.""" +from __future__ import annotations + +from pydeconz.group import DeconzGroup as Group from pydeconz.light import Light from homeassistant.components.light import ( @@ -9,13 +12,16 @@ from homeassistant.components.light import ( ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, DOMAIN, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, @@ -23,7 +29,6 @@ from homeassistant.components.light import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util from .const import ( COVER_TYPES, @@ -106,24 +111,50 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) + self._attr_supported_color_modes = set() self.update_features(self._device) - def update_features(self, device): + def update_features(self, device: Light | Group) -> None: """Calculate supported features of device.""" + supported_color_modes = self._attr_supported_color_modes + + if device.ct is not None: + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + + if device.hue is not None and device.sat is not None: + supported_color_modes.add(COLOR_MODE_HS) + + if device.xy is not None: + supported_color_modes.add(COLOR_MODE_XY) + + if not supported_color_modes and device.brightness is not None: + supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + + if not supported_color_modes: + supported_color_modes.add(COLOR_MODE_ONOFF) + if device.brightness is not None: - self._attr_supported_features |= SUPPORT_BRIGHTNESS self._attr_supported_features |= SUPPORT_FLASH self._attr_supported_features |= SUPPORT_TRANSITION - if device.ct is not None: - self._attr_supported_features |= SUPPORT_COLOR_TEMP - - if device.xy is not None or (device.hue is not None and device.sat is not None): - self._attr_supported_features |= SUPPORT_COLOR - if device.effect is not None: self._attr_supported_features |= SUPPORT_EFFECT + @property + def color_mode(self) -> str: + """Return the color mode of the light.""" + if self._device.colormode == "ct": + color_mode = COLOR_MODE_COLOR_TEMP + elif self._device.colormode == "hs": + color_mode = COLOR_MODE_HS + elif self._device.colormode == "xy": + color_mode = COLOR_MODE_XY + elif self._device.brightness is not None: + color_mode = COLOR_MODE_BRIGHTNESS + else: + color_mode = COLOR_MODE_ONOFF + return color_mode + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -137,20 +168,17 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def color_temp(self): """Return the CT color value.""" - if self._device.colormode != "ct": - return None - return self._device.ct @property - def hs_color(self): + def hs_color(self) -> tuple: """Return the hs color value.""" - if self._device.colormode in ("xy", "hs"): - if self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) - if self._device.hue and self._device.sat: - return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) - return None + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) + + @property + def xy_color(self) -> tuple | None: + """Return the XY color value.""" + return self._device.xy @property def is_on(self): @@ -161,18 +189,18 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Turn on light.""" data = {"on": True} + if ATTR_BRIGHTNESS in kwargs: + data["bri"] = kwargs[ATTR_BRIGHTNESS] + if ATTR_COLOR_TEMP in kwargs: data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - if self._device.xy is not None: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - else: - data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) - if ATTR_BRIGHTNESS in kwargs: - data["bri"] = kwargs[ATTR_BRIGHTNESS] + if ATTR_XY_COLOR in kwargs: + data["xy"] = kwargs[ATTR_XY_COLOR] if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) @@ -250,6 +278,21 @@ class DeconzGroup(DeconzBaseLight): if light.ZHATYPE == Light.ZHATYPE: self.update_features(light) + for exclusive_color_mode in [COLOR_MODE_ONOFF, COLOR_MODE_BRIGHTNESS]: + if ( + exclusive_color_mode in self._attr_supported_color_modes + and len(self._attr_supported_color_modes) > 1 + ): + self._attr_supported_color_modes.remove(exclusive_color_mode) + + @property + def hs_color(self) -> tuple | None: + """Return the hs color value.""" + try: + return super().hs_color + except TypeError: + return None + @property def unique_id(self): """Return a unique identifier for this device.""" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 6ec73085322..28d7b1c59bd 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -7,13 +7,19 @@ import pytest from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, DOMAIN as LIGHT_DOMAIN, EFFECT_COLORLOOP, FLASH_LONG, @@ -73,7 +79,7 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): "bri": 255, "colormode": "xy", "effect": "colorloop", - "xy": (500, 500), + "xy": (0.5, 0.5), "reachable": True, }, "type": "Extended color light", @@ -117,16 +123,22 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == STATE_ON assert rgb_light.attributes[ATTR_BRIGHTNESS] == 255 - assert rgb_light.attributes[ATTR_HS_COLOR] == (224.235, 100.0) + assert rgb_light.attributes[ATTR_XY_COLOR] == (0.5, 0.5) + assert rgb_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_XY] + assert rgb_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_XY + assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 44 assert rgb_light.attributes["is_deconz_group"] is False - assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 61 tunable_white_light = hass.states.get("light.tunable_white_light") assert tunable_white_light.state == STATE_ON assert tunable_white_light.attributes[ATTR_COLOR_TEMP] == 2500 assert tunable_white_light.attributes[ATTR_MAX_MIREDS] == 454 assert tunable_white_light.attributes[ATTR_MIN_MIREDS] == 155 - assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert tunable_white_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + COLOR_MODE_COLOR_TEMP + ] + assert tunable_white_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 tunable_white_light_bad_maxmin = hass.states.get( "light.tunable_white_light_with_bad_maxmin_values" @@ -135,10 +147,12 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): assert tunable_white_light_bad_maxmin.attributes[ATTR_COLOR_TEMP] == 2500 assert tunable_white_light_bad_maxmin.attributes[ATTR_MAX_MIREDS] == 650 assert tunable_white_light_bad_maxmin.attributes[ATTR_MIN_MIREDS] == 140 - assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 0 on_off_light = hass.states.get("light.on_off_light") assert on_off_light.state == STATE_ON + assert on_off_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_ONOFF] + assert on_off_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_ONOFF assert on_off_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert hass.states.get("light.light_group").state == STATE_ON @@ -191,7 +205,7 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.rgb_light", - ATTR_HS_COLOR: (20, 30), + ATTR_XY_COLOR: (0.411, 0.351), ATTR_FLASH: FLASH_LONG, ATTR_EFFECT: "None", }, @@ -598,4 +612,4 @@ async def test_verify_group_supported_features(hass, aioclient_mock): assert len(hass.states.async_all()) == 4 assert hass.states.get("light.group").state == STATE_ON - assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 63 + assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 44 From 181a4519b873c4870fa81aa6c998fb97f98b6d51 Mon Sep 17 00:00:00 2001 From: tkdrob Date: Thu, 10 Jun 2021 03:56:35 -0400 Subject: [PATCH 277/750] Clean up unloads (#51688) --- homeassistant/components/modern_forms/__init__.py | 12 +----------- homeassistant/components/wallbox/__init__.py | 10 +--------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 7530f7658c7..3d5a7c50315 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -1,7 +1,6 @@ """The Modern Forms integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -63,16 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Modern Forms config entry.""" - - # Unload entities for this entry/device. - unload_ok = all( - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 275ccddde33..63fe37732dc 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -1,5 +1,4 @@ """The Wallbox integration.""" -import asyncio from datetime import timedelta import logging @@ -115,14 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN]["connections"].pop(entry.entry_id) From c1bc99890d2b2a6547e15322c5753852a10b8b0d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Jun 2021 12:46:28 +0200 Subject: [PATCH 278/750] Improve editing of device triggers referencing non-added cover (#51703) --- .../components/cover/device_trigger.py | 8 +- tests/components/cover/test_device_trigger.py | 213 +++++------------- 2 files changed, 64 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 9b94833bb29..825a6869287 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -10,7 +10,6 @@ from homeassistant.components.homeassistant.triggers import ( state as state_trigger, ) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, CONF_ABOVE, CONF_BELOW, CONF_DEVICE_ID, @@ -27,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType from . import ( @@ -77,11 +77,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: - continue - - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) # Add triggers for each entity that belongs to this integration diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 8732fcc8020..b7f15de1e3c 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -4,7 +4,12 @@ from datetime import timedelta import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import DOMAIN +from homeassistant.components.cover import ( + DOMAIN, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) from homeassistant.const import ( CONF_PLATFORM, STATE_CLOSED, @@ -47,65 +52,47 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): - """Test we get the expected triggers from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "opened", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) - - -async def test_get_triggers_set_pos( - hass, device_reg, entity_reg, enable_custom_integrations +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_trigger_types", + [ + (False, SUPPORT_OPEN, 0, ["opened", "closed", "opening", "closing"]), + ( + False, + SUPPORT_OPEN | SUPPORT_SET_POSITION, + 0, + ["opened", "closed", "opening", "closing", "position"], + ), + ( + False, + SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION, + 0, + ["opened", "closed", "opening", "closing", "tilt_position"], + ), + (True, 0, SUPPORT_OPEN, ["opened", "closed", "opening", "closing"]), + ( + True, + 0, + SUPPORT_OPEN | SUPPORT_SET_POSITION, + ["opened", "closed", "opening", "closing", "position"], + ), + ( + True, + 0, + SUPPORT_OPEN | SUPPORT_SET_TILT_POSITION, + ["opened", "closed", "opening", "closing", "tilt_position"], + ), + ], +) +async def test_get_triggers( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_trigger_types, ): """Test we get the expected triggers from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] - config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -113,106 +100,30 @@ async def test_get_triggers_set_pos( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + "attributes", + {"supported_features": features_state}, + ) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "opened", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "position", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - ] - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, expected_triggers) + expected_triggers = [] - -async def test_get_triggers_set_tilt_pos( - hass, device_reg, entity_reg, enable_custom_integrations -): - """Test we get the expected triggers from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[2] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_triggers = [ + expected_triggers += [ { "platform": "device", "domain": DOMAIN, - "type": "opened", + "type": trigger, "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "tilt_position", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for trigger in expected_trigger_types ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, expected_triggers) From d9110b5208203271cea6c0366d0e8756885879e2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Jun 2021 13:06:01 +0200 Subject: [PATCH 279/750] Improve editing of device triggers referencing non-added binary sensors (#51700) --- .../binary_sensor/device_trigger.py | 8 ++-- .../binary_sensor/test_device_trigger.py | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index b87a761a7a1..0e41db85763 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -7,8 +7,9 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_ON, ) from homeassistant.components.homeassistant.triggers import state as state_trigger -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.entity_registry import async_entries_for_device from . import ( @@ -220,10 +221,7 @@ async def async_get_triggers(hass, device_id): ] for entry in entries: - device_class = DEVICE_CLASS_NONE - state = hass.states.get(entry.entity_id) - if state: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 0e5cbcc1d70..1dbed7d19e1 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -78,6 +78,46 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat assert triggers == expected_triggers +async def test_get_triggers_no_state( + hass, device_reg, entity_reg, enable_custom_integrations +): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + entity_ids = {} + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_ids[device_class] = entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + device_class=device_class, + ).entity_id + + await hass.async_block_till_done() + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": entity_ids[device_class], + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + async def test_get_trigger_capabilities(hass, device_reg, entity_reg): """Test we get the expected capabilities from a binary_sensor trigger.""" config_entry = MockConfigEntry(domain="test", data={}) From 79996682e570342272675c88dffdc0b25ac70242 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Jun 2021 13:17:48 +0200 Subject: [PATCH 280/750] =?UTF-8?q?Add=20device=20trigger=20for=20IKEA=20T?= =?UTF-8?q?r=C3=A5dfri=20Shortcut=20button=20to=20deCONZ=20(#51680)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/deconz/device_trigger.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 2703adbc139..7df6fec4615 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -205,6 +205,13 @@ TRADFRI_REMOTE = { (CONF_LONG_RELEASE, CONF_RIGHT): {CONF_EVENT: 5003}, } +TRADFRI_SHORTCUT_REMOTE_MODEL = "TRADFRI SHORTCUT Button" +TRADFRI_SHORTCUT_REMOTE = { + (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, ""): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, ""): {CONF_EVENT: 1003}, +} + TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer" TRADFRI_WIRELESS_DIMMER = { (CONF_ROTATED_FAST, CONF_LEFT): {CONF_EVENT: 4002}, @@ -534,6 +541,7 @@ REMOTES = { TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, + TRADFRI_SHORTCUT_REMOTE_MODEL: TRADFRI_SHORTCUT_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, AQARA_CUBE_MODEL_ALT1: AQARA_CUBE, From fca0446ff8cf83da9cb39cfba37b53a85bbb7d84 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Jun 2021 14:18:09 +0200 Subject: [PATCH 281/750] Add pollen sensors to Ambee (#51702) --- homeassistant/components/ambee/__init__.py | 25 +- homeassistant/components/ambee/const.py | 231 ++++++++++++--- homeassistant/components/ambee/models.py | 15 + homeassistant/components/ambee/sensor.py | 86 ++++-- .../components/ambee/strings.sensor.json | 10 + .../ambee/translations/sensor.en.json | 10 + tests/components/ambee/conftest.py | 5 +- tests/components/ambee/test_init.py | 6 +- tests/components/ambee/test_sensor.py | 265 ++++++++++++++++-- tests/fixtures/ambee/pollen.json | 43 +++ 10 files changed, 585 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/ambee/models.py create mode 100644 homeassistant/components/ambee/strings.sensor.json create mode 100644 homeassistant/components/ambee/translations/sensor.en.json create mode 100644 tests/fixtures/ambee/pollen.json diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index cea586d1a67..4968420174e 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -9,30 +9,31 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, LOGGER, SCAN_INTERVAL +from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN PLATFORMS = (SENSOR_DOMAIN,) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambee from a config entry.""" + hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) + client = Ambee( api_key=entry.data[CONF_API_KEY], latitude=entry.data[CONF_LATITUDE], longitude=entry.data[CONF_LONGITUDE], ) - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - update_method=client.air_quality, - ) - await coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + for service in {SERVICE_AIR_QUALITY, SERVICE_POLLEN}: + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + update_method=getattr(client, service), + ) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][service] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 56107131f34..c30d1f8eadc 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -3,67 +3,210 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Final +from typing import Final from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_NAME, - ATTR_SERVICE, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO, ) +from .models import AmbeeSensor + DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(minutes=180) -SERVICE_AIR_QUALITY: Final = ("air_quality", "Air Quality") +ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default" +ATTR_ENTRY_TYPE: Final = "entry_type" +ENTRY_TYPE_SERVICE: Final = "service" -SENSORS: dict[str, dict[str, Any]] = { - "particulate_matter_2_5": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Particulate Matter < 2.5 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, +DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" + +SERVICE_AIR_QUALITY: Final = "air_quality" +SERVICE_POLLEN: Final = "pollen" + +SERVICES: dict[str, str] = { + SERVICE_AIR_QUALITY: "Air Quality", + SERVICE_POLLEN: "Pollen", +} + +SENSORS: dict[str, dict[str, AmbeeSensor]] = { + SERVICE_AIR_QUALITY: { + "particulate_matter_2_5": { + ATTR_NAME: "Particulate Matter < 2.5 μm", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "particulate_matter_10": { + ATTR_NAME: "Particulate Matter < 10 μm", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "sulphur_dioxide": { + ATTR_NAME: "Sulphur Dioxide (SO2)", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "nitrogen_dioxide": { + ATTR_NAME: "Nitrogen Dioxide (NO2)", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "ozone": { + ATTR_NAME: "Ozone", + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "carbon_monoxide": { + ATTR_NAME: "Carbon Monoxide (CO)", + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + "air_quality_index": { + ATTR_NAME: "Air Quality Index (AQI)", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, }, - "particulate_matter_10": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Particulate Matter < 10 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "sulphur_dioxide": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Sulphur Dioxide (SO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "nitrogen_dioxide": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Nitrogen Dioxide (NO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "ozone": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Ozone", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "carbon_monoxide": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Carbon Monoxide (CO)", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "air_quality_index": { - ATTR_SERVICE: SERVICE_AIR_QUALITY, - ATTR_NAME: "Air Quality Index (AQI)", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + SERVICE_POLLEN: { + "grass": { + ATTR_NAME: "Grass Pollen", + ATTR_ICON: "mdi:grass", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "tree": { + ATTR_NAME: "Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "weed": { + ATTR_NAME: "Weed Pollen", + ATTR_ICON: "mdi:sprout", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + }, + "grass_risk": { + ATTR_NAME: "Grass Pollen Risk", + ATTR_ICON: "mdi:grass", + ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, + }, + "tree_risk": { + ATTR_NAME: "Tree Pollen Risk", + ATTR_ICON: "mdi:tree", + ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, + }, + "weed_risk": { + ATTR_NAME: "Weed Pollen Risk", + ATTR_ICON: "mdi:sprout", + ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, + }, + "grass_poaceae": { + ATTR_NAME: "Poaceae Grass Pollen", + ATTR_ICON: "mdi:grass", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_alder": { + ATTR_NAME: "Alder Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_birch": { + ATTR_NAME: "Birch Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_cypress": { + ATTR_NAME: "Cypress Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_elm": { + ATTR_NAME: "Elm Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_hazel": { + ATTR_NAME: "Hazel Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_oak": { + ATTR_NAME: "Oak Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_pine": { + ATTR_NAME: "Pine Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_plane": { + ATTR_NAME: "Plane Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "tree_poplar": { + ATTR_NAME: "Poplar Tree Pollen", + ATTR_ICON: "mdi:tree", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "weed_chenopod": { + ATTR_NAME: "Chenopod Weed Pollen", + ATTR_ICON: "mdi:sprout", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "weed_mugwort": { + ATTR_NAME: "Mugwort Weed Pollen", + ATTR_ICON: "mdi:sprout", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "weed_nettle": { + ATTR_NAME: "Nettle Weed Pollen", + ATTR_ICON: "mdi:sprout", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, + "weed_ragweed": { + ATTR_NAME: "Ragweed Weed Pollen", + ATTR_ICON: "mdi:sprout", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, + ATTR_ENABLED_BY_DEFAULT: False, + }, }, } diff --git a/homeassistant/components/ambee/models.py b/homeassistant/components/ambee/models.py new file mode 100644 index 00000000000..871aeed332b --- /dev/null +++ b/homeassistant/components/ambee/models.py @@ -0,0 +1,15 @@ +"""Models helper class for the Ambee integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class AmbeeSensor(TypedDict, total=False): + """Represent an Ambee Sensor.""" + + device_class: str + enabled_by_default: bool + icon: str + name: str + state_class: str + unit_of_measurement: str diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index 0bb626afb62..54e67160822 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -1,18 +1,21 @@ """Support for Ambee sensors.""" from __future__ import annotations -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME, - ATTR_SERVICE, ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -20,7 +23,15 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN, SENSORS +from .const import ( + ATTR_ENABLED_BY_DEFAULT, + ATTR_ENTRY_TYPE, + DOMAIN, + ENTRY_TYPE_SERVICE, + SENSORS, + SERVICES, +) +from .models import AmbeeSensor async def async_setup_entry( @@ -28,42 +39,61 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up Ambee sensor based on a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + """Set up Ambee sensors based on a config entry.""" async_add_entities( - AmbeeSensor(coordinator=coordinator, entry_id=entry.entry_id, key=sensor) - for sensor in SENSORS + AmbeeSensorEntity( + coordinator=hass.data[DOMAIN][entry.entry_id][service_key], + entry_id=entry.entry_id, + sensor_key=sensor_key, + sensor=sensor, + service_key=service_key, + service=SERVICES[service_key], + ) + for service_key, service_sensors in SENSORS.items() + for sensor_key, sensor in service_sensors.items() ) -class AmbeeSensor(CoordinatorEntity, SensorEntity): +class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): """Defines an Ambee sensor.""" def __init__( - self, *, coordinator: DataUpdateCoordinator, entry_id: str, key: str + self, + *, + coordinator: DataUpdateCoordinator, + entry_id: str, + sensor_key: str, + sensor: AmbeeSensor, + service_key: str, + service: str, ) -> None: """Initialize Ambee sensor.""" super().__init__(coordinator=coordinator) - self._key = key - self._entry_id = entry_id - self._service_key, self._service_name = SENSORS[key][ATTR_SERVICE] + self._sensor_key = sensor_key + self._service_key = service_key - self._attr_device_class = SENSORS[key].get(ATTR_DEVICE_CLASS) - self._attr_name = SENSORS[key][ATTR_NAME] - self._attr_state_class = SENSORS[key].get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{entry_id}_{key}" - self._attr_unit_of_measurement = SENSORS[key].get(ATTR_UNIT_OF_MEASUREMENT) + self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}" + self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) + self._attr_entity_registry_enabled_default = sensor.get( + ATTR_ENABLED_BY_DEFAULT, True + ) + self._attr_icon = sensor.get(ATTR_ICON) + self._attr_name = sensor.get(ATTR_NAME) + self._attr_state_class = sensor.get(ATTR_STATE_CLASS) + self._attr_unique_id = f"{entry_id}_{service_key}_{sensor_key}" + self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) + + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")}, + ATTR_NAME: service, + ATTR_MANUFACTURER: "Ambee", + ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, + } @property def state(self) -> StateType: """Return the state of the sensor.""" - return getattr(self.coordinator.data, self._key) # type: ignore[no-any-return] - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this Ambee Service.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, f"{self._entry_id}_{self._service_key}")}, - ATTR_NAME: self._service_name, - ATTR_MANUFACTURER: "Ambee", - } + value = getattr(self.coordinator.data, self._sensor_key) + if isinstance(value, str): + return value.lower() + return value # type: ignore[no-any-return] diff --git a/homeassistant/components/ambee/strings.sensor.json b/homeassistant/components/ambee/strings.sensor.json new file mode 100644 index 00000000000..83eb3b3fd73 --- /dev/null +++ b/homeassistant/components/ambee/strings.sensor.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "low": "Low", + "moderate": "Moderate", + "high": "High", + "very high": "Very High" + } + } +} diff --git a/homeassistant/components/ambee/translations/sensor.en.json b/homeassistant/components/ambee/translations/sensor.en.json new file mode 100644 index 00000000000..a4b198eadf5 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.en.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "High", + "low": "Low", + "moderate": "Moderate", + "very high": "Very High" + } + } +} \ No newline at end of file diff --git a/tests/components/ambee/conftest.py b/tests/components/ambee/conftest.py index de88e28e1d1..d6dd53a9711 100644 --- a/tests/components/ambee/conftest.py +++ b/tests/components/ambee/conftest.py @@ -2,7 +2,7 @@ import json from unittest.mock import AsyncMock, MagicMock, patch -from ambee import AirQuality +from ambee import AirQuality, Pollen import pytest from homeassistant.components.ambee.const import DOMAIN @@ -34,6 +34,9 @@ def mock_ambee(aioclient_mock: AiohttpClientMocker): json.loads(load_fixture("ambee/air_quality.json")) ) ) + client.pollen = AsyncMock( + return_value=Pollen.from_dict(json.loads(load_fixture("ambee/pollen.json"))) + ) yield ambee_mock diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py index c58e7cfef0d..5db3255dc53 100644 --- a/tests/components/ambee/test_init.py +++ b/tests/components/ambee/test_init.py @@ -29,11 +29,11 @@ async def test_load_unload_config_entry( @patch( - "homeassistant.components.ambee.Ambee.air_quality", + "homeassistant.components.ambee.Ambee.request", side_effect=AmbeeConnectionError, ) async def test_config_entry_not_ready( - mock_air_quality: MagicMock, + mock_request: MagicMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry, ) -> None: @@ -42,5 +42,5 @@ async def test_config_entry_not_ready( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_air_quality.call_count == 1 + assert mock_request.call_count == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index a754256ff0a..34eaa273901 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -1,12 +1,26 @@ """Tests for the sensors provided by the Ambee integration.""" -from homeassistant.components.ambee.const import DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.ambee.const import ( + DEVICE_CLASS_AMBEE_RISK, + DOMAIN, + ENTRY_TYPE_SERVICE, +) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO, ) @@ -25,11 +39,11 @@ async def test_air_quality( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.particulate_matter_2_5_mm") - entry = entity_registry.async_get("sensor.particulate_matter_2_5_mm") + state = hass.states.get("sensor.air_quality_particulate_matter_2_5") + entry = entity_registry.async_get("sensor.air_quality_particulate_matter_2_5") assert entry assert state - assert entry.unique_id == f"{entry_id}_particulate_matter_2_5" + assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" assert state.state == "3.14" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -38,12 +52,13 @@ async def test_air_quality( == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.particulate_matter_10_mm") - entry = entity_registry.async_get("sensor.particulate_matter_10_mm") + state = hass.states.get("sensor.air_quality_particulate_matter_10") + entry = entity_registry.async_get("sensor.air_quality_particulate_matter_10") assert entry assert state - assert entry.unique_id == f"{entry_id}_particulate_matter_10" + assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" assert state.state == "5.24" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -52,12 +67,13 @@ async def test_air_quality( == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.sulphur_dioxide_so2") - entry = entity_registry.async_get("sensor.sulphur_dioxide_so2") + state = hass.states.get("sensor.air_quality_sulphur_dioxide") + entry = entity_registry.async_get("sensor.air_quality_sulphur_dioxide") assert entry assert state - assert entry.unique_id == f"{entry_id}_sulphur_dioxide" + assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" assert state.state == "0.031" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -66,12 +82,13 @@ async def test_air_quality( == CONCENTRATION_PARTS_PER_BILLION ) assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.nitrogen_dioxide_no2") - entry = entity_registry.async_get("sensor.nitrogen_dioxide_no2") + state = hass.states.get("sensor.air_quality_nitrogen_dioxide") + entry = entity_registry.async_get("sensor.air_quality_nitrogen_dioxide") assert entry assert state - assert entry.unique_id == f"{entry_id}_nitrogen_dioxide" + assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" assert state.state == "0.66" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -80,12 +97,13 @@ async def test_air_quality( == CONCENTRATION_PARTS_PER_BILLION ) assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.ozone") - entry = entity_registry.async_get("sensor.ozone") + state = hass.states.get("sensor.air_quality_ozone") + entry = entity_registry.async_get("sensor.air_quality_ozone") assert entry assert state - assert entry.unique_id == f"{entry_id}_ozone" + assert entry.unique_id == f"{entry_id}_air_quality_ozone" assert state.state == "17.067" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -94,12 +112,13 @@ async def test_air_quality( == CONCENTRATION_PARTS_PER_BILLION ) assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.carbon_monoxide_co") - entry = entity_registry.async_get("sensor.carbon_monoxide_co") + state = hass.states.get("sensor.air_quality_carbon_monoxide") + entry = entity_registry.async_get("sensor.air_quality_carbon_monoxide") assert entry assert state - assert entry.unique_id == f"{entry_id}_carbon_monoxide" + assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" assert state.state == "0.105" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" @@ -108,17 +127,19 @@ async def test_air_quality( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_PARTS_PER_MILLION ) + assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.air_quality_index_aqi") - entry = entity_registry.async_get("sensor.air_quality_index_aqi") + state = hass.states.get("sensor.air_quality_air_quality_index") + entry = entity_registry.async_get("sensor.air_quality_air_quality_index") assert entry assert state - assert entry.unique_id == f"{entry_id}_air_quality_index" + assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" assert state.state == "13" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_ICON not in state.attributes assert entry.device_id device_entry = device_registry.async_get(entry.device_id) @@ -126,5 +147,203 @@ async def test_air_quality( assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} assert device_entry.manufacturer == "Ambee" assert device_entry.name == "Air Quality" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE assert not device_entry.model assert not device_entry.sw_version + + +async def test_pollen( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Ambee Pollen sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.pollen_grass") + entry = entity_registry.async_get("sensor.pollen_grass") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_grass" + assert state.state == "190" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen" + assert state.attributes.get(ATTR_ICON) == "mdi:grass" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.pollen_tree") + entry = entity_registry.async_get("sensor.pollen_tree") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_tree" + assert state.state == "127" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen" + assert state.attributes.get(ATTR_ICON) == "mdi:tree" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.pollen_weed") + entry = entity_registry.async_get("sensor.pollen_weed") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_weed" + assert state.state == "95" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen" + assert state.attributes.get(ATTR_ICON) == "mdi:sprout" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.pollen_grass_risk") + entry = entity_registry.async_get("sensor.pollen_grass_risk") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_grass_risk" + assert state.state == "high" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk" + assert state.attributes.get(ATTR_ICON) == "mdi:grass" + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.pollen_tree_risk") + entry = entity_registry.async_get("sensor.pollen_tree_risk") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_tree_risk" + assert state.state == "moderate" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk" + assert state.attributes.get(ATTR_ICON) == "mdi:tree" + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.pollen_weed_risk") + entry = entity_registry.async_get("sensor.pollen_weed_risk") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_weed_risk" + assert state.state == "high" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk" + assert state.attributes.get(ATTR_ICON) == "mdi:sprout" + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_pollen")} + assert device_entry.manufacturer == "Ambee" + assert device_entry.name == "Pollen" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.pollen_grass_poaceae", + "sensor.pollen_tree_alder", + "sensor.pollen_tree_birch", + "sensor.pollen_tree_cypress", + "sensor.pollen_tree_elm", + "sensor.pollen_tree_hazel", + "sensor.pollen_tree_oak", + "sensor.pollen_tree_pine", + "sensor.pollen_tree_plane", + "sensor.pollen_tree_poplar", + "sensor.pollen_weed_chenopod", + "sensor.pollen_weed_mugwort", + "sensor.pollen_weed_nettle", + "sensor.pollen_weed_ragweed", + ), +) +async def test_pollen_disabled_by_default( + hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str +) -> None: + """Test the Ambee Pollen sensors that are disabled by default.""" + entity_registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state is None + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION + + +@pytest.mark.parametrize( + "key,icon,name,value", + [ + ("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"), + ("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"), + ("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"), + ("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"), + ("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"), + ("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"), + ("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"), + ("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"), + ("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"), + ("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"), + ("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"), + ("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"), + ("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"), + ("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"), + ], +) +async def test_pollen_enable_disable_by_defaults( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ambee: AsyncMock, + key: str, + icon: str, + name: str, + value: str, +) -> None: + """Test the Ambee Pollen sensors that are disabled by default.""" + entry_id = mock_config_entry.entry_id + entity_id = f"{SENSOR_DOMAIN}.pollen_{key}" + entity_registry = er.async_get(hass) + + # Pre-create registry entry for disabled by default sensor + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{entry_id}_pollen_{key}", + suggested_object_id=f"pollen_{key}", + disabled_by=None, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + entry = entity_registry.async_get(entity_id) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_pollen_{key}" + assert state.state == value + assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert state.attributes.get(ATTR_ICON) == icon + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/fixtures/ambee/pollen.json b/tests/fixtures/ambee/pollen.json new file mode 100644 index 00000000000..95f8a96c3c8 --- /dev/null +++ b/tests/fixtures/ambee/pollen.json @@ -0,0 +1,43 @@ +{ + "message": "Success", + "lat": 52.42, + "lng": 6.42, + "data": [ + { + "Count": { + "grass_pollen": 190, + "tree_pollen": 127, + "weed_pollen": 95 + }, + "Risk": { + "grass_pollen": "High", + "tree_pollen": "Moderate", + "weed_pollen": "High" + }, + "Species": { + "Grass": { + "Grass / Poaceae": 190 + }, + "Others": 5, + "Tree": { + "Alder": 0, + "Birch": 35, + "Cypress": 0, + "Elm": 0, + "Hazel": 0, + "Oak": 55, + "Pine": 30, + "Plane": 5, + "Poplar / Cottonwood": 0 + }, + "Weed": { + "Chenopod": 0, + "Mugwort": 1, + "Nettle": 88, + "Ragweed": 3 + } + }, + "updatedAt": "2021-06-09T16:24:27.000Z" + } + ] +} \ No newline at end of file From 220ad2baeab90e568d63719c3483b560d081c63f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 10 Jun 2021 14:42:28 +0200 Subject: [PATCH 282/750] Use attrs instead of properties in Nettigo Air Monitor integration (#51705) * Use attrs instead of properties * Remove unused self Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/nam/air_quality.py | 19 ++------- homeassistant/components/nam/sensor.py | 47 ++++----------------- 2 files changed, 12 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py index 163b50148db..4f560740937 100644 --- a/homeassistant/components/nam/air_quality.py +++ b/homeassistant/components/nam/air_quality.py @@ -4,7 +4,6 @@ from __future__ import annotations from homeassistant.components.air_quality import AirQualityEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -44,13 +43,11 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: """Initialize.""" super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_name = f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[sensor_type]}" + self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}" self.sensor_type = sensor_type - @property - def name(self) -> str: - """Return the name.""" - return f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[self.sensor_type]}" - @property def particulate_matter_2_5(self) -> StateType: """Return the particulate matter 2.5 level.""" @@ -72,16 +69,6 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None) ) - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return f"{self.coordinator.unique_id}-{self.sensor_type}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return self.coordinator.device_info - @property def available(self) -> bool: """Return if entity is available.""" diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 30982d8571d..6adb29d3efb 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow @@ -44,50 +43,22 @@ class NAMSensor(CoordinatorEntity, SensorEntity): def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: """Initialize.""" super().__init__(coordinator) + description = SENSORS[sensor_type] + self._attr_device_class = description[ATTR_DEVICE_CLASS] + self._attr_device_info = coordinator.device_info + self._attr_entity_registry_enabled_default = description[ATTR_ENABLED] + self._attr_icon = description[ATTR_ICON] + self._attr_name = description[ATTR_LABEL] + self._attr_state_class = description[ATTR_STATE_CLASS] + self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}" + self._attr_unit_of_measurement = description[ATTR_UNIT] self.sensor_type = sensor_type - self._description = SENSORS[sensor_type] - self._attr_state_class = SENSORS[sensor_type][ATTR_STATE_CLASS] - - @property - def name(self) -> str: - """Return the name.""" - return self._description[ATTR_LABEL] @property def state(self) -> Any: """Return the state.""" return getattr(self.coordinator.data, self.sensor_type) - @property - def unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - return self._description[ATTR_UNIT] - - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return self._description[ATTR_DEVICE_CLASS] - - @property - def icon(self) -> str | None: - """Return the icon.""" - return self._description[ATTR_ICON] - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._description[ATTR_ENABLED] - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return f"{self.coordinator.unique_id}-{self.sensor_type}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return self.coordinator.device_info - @property def available(self) -> bool: """Return if entity is available.""" From 17cf0cef0f228b3a29612bc5a13165e67936e920 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Jun 2021 15:08:35 +0200 Subject: [PATCH 283/750] Increase Ambee update interval to 1 hour (#51708) --- homeassistant/components/ambee/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index c30d1f8eadc..730c6780f4f 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -22,7 +22,7 @@ from .models import AmbeeSensor DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(minutes=180) +SCAN_INTERVAL = timedelta(hours=1) ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default" ATTR_ENTRY_TYPE: Final = "entry_type" From d75c97cbf3ca64fbd397cee6d50e6c10756841fc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 10 Jun 2021 15:41:42 +0100 Subject: [PATCH 284/750] Revert "Set Fahrenheit reporting precision to tenths for Homekit Controller climate entities (#50415)" (#51698) --- .../components/homekit_controller/climate.py | 12 +----------- tests/components/homekit_controller/test_climate.py | 8 -------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index a2c9c2540ac..2c251d41fb3 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -36,7 +36,7 @@ from homeassistant.components.climate.const import ( SWING_OFF, SWING_VERTICAL, ) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -323,11 +323,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): """Return the unit of measurement.""" return TEMP_CELSIUS - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" @@ -541,11 +536,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Return the unit of measurement.""" return TEMP_CELSIUS - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_TENTHS - ENTITY_TYPES = { ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index bc9fdaa1013..52671703cca 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,6 +1,4 @@ """Basic checks for HomeKitclimate.""" -from unittest.mock import patch - from aiohomekit.model.characteristics import ( ActivationStateValues, CharacteristicsTypes, @@ -21,7 +19,6 @@ from homeassistant.components.climate.const import ( SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) -from homeassistant.const import TEMP_FAHRENHEIT from tests.components.homekit_controller.common import setup_test_component @@ -448,11 +445,6 @@ async def test_climate_read_thermostat_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT_COOL - # Ensure converted Fahrenheit precision is reported in tenths - with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): - state = await helper.poll_and_get_state() - assert state.attributes["current_temperature"] == 69.8 - async def test_hvac_mode_vs_hvac_action(hass, utcnow): """Check that we haven't conflated hvac_mode and hvac_action.""" From a7f05713a0b2c91a16f92bdb1d38e50a322a35ff Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 10 Jun 2021 18:08:25 +0200 Subject: [PATCH 285/750] Add Supervisor restart add-on helper (#51717) --- homeassistant/components/hassio/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index d391817f964..419e6865b69 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -222,6 +222,18 @@ async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: return await hassio.send_command(command, timeout=60) +@bind_hass +@api_data +async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict: + """Restart add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/restart" + return await hassio.send_command(command, timeout=None) + + @bind_hass @api_data async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: From 76c3058d15f8cd07890df9f6b962f107f5059f2d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Jun 2021 19:11:38 +0200 Subject: [PATCH 286/750] Rename device trigger base schema to DEVICE_TRIGGER_BASE_SCHEMA (#51719) --- .../components/alarm_control_panel/device_trigger.py | 4 ++-- homeassistant/components/arcam_fmj/device_trigger.py | 4 ++-- homeassistant/components/binary_sensor/device_trigger.py | 4 ++-- homeassistant/components/climate/device_trigger.py | 6 +++--- homeassistant/components/cover/device_trigger.py | 6 +++--- homeassistant/components/deconz/device_trigger.py | 4 ++-- homeassistant/components/device_automation/__init__.py | 2 +- homeassistant/components/device_automation/toggle_entity.py | 4 ++-- homeassistant/components/device_automation/trigger.py | 4 ++-- homeassistant/components/device_tracker/device_trigger.py | 4 ++-- .../components/homekit_controller/device_trigger.py | 4 ++-- homeassistant/components/hue/device_trigger.py | 4 ++-- homeassistant/components/humidifier/device_trigger.py | 4 ++-- homeassistant/components/kodi/device_trigger.py | 4 ++-- homeassistant/components/lock/device_trigger.py | 4 ++-- homeassistant/components/lutron_caseta/device_trigger.py | 4 ++-- homeassistant/components/media_player/device_trigger.py | 4 ++-- homeassistant/components/mqtt/device_trigger.py | 4 ++-- homeassistant/components/nest/device_trigger.py | 4 ++-- homeassistant/components/netatmo/device_trigger.py | 4 ++-- homeassistant/components/philips_js/device_trigger.py | 4 ++-- homeassistant/components/sensor/device_trigger.py | 4 ++-- homeassistant/components/shelly/device_trigger.py | 4 ++-- homeassistant/components/tasmota/device_trigger.py | 4 ++-- homeassistant/components/vacuum/device_trigger.py | 4 ++-- homeassistant/components/zha/device_trigger.py | 4 ++-- .../templates/device_trigger/integration/device_trigger.py | 4 ++-- 27 files changed, 55 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 477a0c0fe6d..cae9161abf9 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_NIGHT, ) from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, @@ -41,7 +41,7 @@ TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | { "armed_night", } -TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 4ae34abb2c2..25ad5a1133f 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, EVENT_TURN_ON TRIGGER_TYPES = {"turn_on"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 0e41db85763..ad5c26ed04f 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,7 +1,7 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, CONF_TURNED_ON, @@ -178,7 +178,7 @@ ENTITY_TRIGGERS = { } -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index df925463d4c..1b5127d7d4a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, state as state_trigger, @@ -32,7 +32,7 @@ TRIGGER_TYPES = { "hvac_mode_changed", } -HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +HVAC_MODE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): "hvac_mode_changed", @@ -41,7 +41,7 @@ HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) CURRENT_TRIGGER_SCHEMA = vol.All( - TRIGGER_BASE_SCHEMA.extend( + DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In( diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 825a6869287..acfd276d1fb 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, state as state_trigger, @@ -41,7 +41,7 @@ POSITION_TRIGGER_TYPES = {"position", "tilt_position"} STATE_TRIGGER_TYPES = {"opened", "closed", "opening", "closing"} POSITION_TRIGGER_SCHEMA = vol.All( - TRIGGER_BASE_SCHEMA.extend( + DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(POSITION_TRIGGER_TYPES), @@ -56,7 +56,7 @@ POSITION_TRIGGER_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), ) -STATE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +STATE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES), diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 7df6fec4615..1784afa76a4 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for deconz events.""" import voluptuous as vol -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -575,7 +575,7 @@ REMOTES = { UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4, } -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 12083a8d139..d6317ce45ce 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -25,7 +25,7 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig DOMAIN = "device_automation" -TRIGGER_BASE_SCHEMA = vol.Schema( +DEVICE_TRIGGER_BASE_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str, diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 72fcc9790b2..cf41fc93d83 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -29,7 +29,7 @@ from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import TRIGGER_BASE_SCHEMA +from . import DEVICE_TRIGGER_BASE_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs @@ -91,7 +91,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( } ) -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index b2892d1abaa..a1b6e53c5c3 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -2,14 +2,14 @@ import voluptuous as vol from homeassistant.components.device_automation import ( - TRIGGER_BASE_SCHEMA, + DEVICE_TRIGGER_BASE_SCHEMA, async_get_device_automation_platform, ) from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_validate_trigger_config(hass, config): diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 3c7f9ac35ad..e73b5a70075 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -6,7 +6,7 @@ from typing import Final import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone from homeassistant.const import ( CONF_DEVICE_ID, @@ -25,7 +25,7 @@ from . import DOMAIN TRIGGER_TYPES: Final[set[str]] = {"enters", "leaves"} -TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index a04d7237cf1..d94552df52d 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -8,7 +8,7 @@ from aiohomekit.utils import clamp_enum_to_char import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -33,7 +33,7 @@ TRIGGER_SUBTYPES = {"single_press", "double_press", "long_press"} CONF_IID = "iid" CONF_SUBTYPE = "subtype" -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES), diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index a5df9d60985..ea91cd07d8c 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for Philips Hue events.""" import voluptuous as vol -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -94,7 +94,7 @@ REMOTES = { HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE, } -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index d0f462f6b0f..98bbe192a1f 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import ( - TRIGGER_BASE_SCHEMA, + DEVICE_TRIGGER_BASE_SCHEMA, toggle_entity, ) from homeassistant.components.homeassistant.triggers import ( @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN TARGET_TRIGGER_SCHEMA = vol.All( - TRIGGER_BASE_SCHEMA.extend( + DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): "target_humidity_changed", diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index b8653290c0d..855d7f7dba9 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -21,7 +21,7 @@ from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON TRIGGER_TYPES = {"turn_on", "turn_off"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 77eb04e3735..2e96b470893 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( CONF_DEVICE_ID, @@ -24,7 +24,7 @@ from . import DOMAIN TRIGGER_TYPES = {"locked", "unlocked"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 230301c12f2..7d9728a79a1 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -33,7 +33,7 @@ from .const import ( SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] -LUTRON_BUTTON_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), } diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index 889bc776962..de0ff6b8e90 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( CONF_DEVICE_ID, @@ -27,7 +27,7 @@ from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off", "idle", "paused", "playing"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 038e6e91523..d9413b80c06 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -8,7 +8,7 @@ import attr import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( CONF_DEVICE, CONF_DEVICE_ID, @@ -54,7 +54,7 @@ MQTT_TRIGGER_BASE = { CONF_DOMAIN: DOMAIN, } -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DEVICE, vol.Required(CONF_DOMAIN): DOMAIN, diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index d59ec05c503..4ed492a15fa 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -20,7 +20,7 @@ DEVICE = "device" TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values()) -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), } diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index d6085ec06ec..b0d4e18b7c9 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -54,7 +54,7 @@ SUBTYPES = { TRIGGER_TYPES = OUTDOOR_CAMERA_TRIGGERS + INDOOR_CAMERA_TRIGGERS + CLIMATE_TRIGGERS -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 77782fc641c..0c45debd384 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry @@ -16,7 +16,7 @@ from .const import DOMAIN TRIGGER_TYPE_TURN_ON = "turn_on" TRIGGER_TYPES = {TRIGGER_TYPE_TURN_ON} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 9729d629d1c..3b00bae816d 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -1,7 +1,7 @@ """Provides device triggers for sensors.""" import voluptuous as vol -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -73,7 +73,7 @@ ENTITY_TRIGGERS = { TRIGGER_SCHEMA = vol.All( - TRIGGER_BASE_SCHEMA.extend( + DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In( diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 05f806dd8e8..e767f49bcbb 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -33,7 +33,7 @@ from .const import ( ) from .utils import get_device_wrapper, get_input_triggers -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES), diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index d4aca9b07ca..6a6f0324e1d 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -9,7 +9,7 @@ from hatasmota.trigger import TasmotaTrigger import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -28,7 +28,7 @@ CONF_DISCOVERY_ID = "discovery_id" CONF_SUBTYPE = "subtype" DEVICE = "device" -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DEVICE, vol.Required(CONF_DOMAIN): DOMAIN, diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index d5c596b209a..4c1d6e93820 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( CONF_DEVICE_ID, @@ -22,7 +22,7 @@ from . import DOMAIN, STATE_CLEANING, STATE_DOCKED TRIGGER_TYPES = {"cleaning", "docked"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 9d04d36f748..03bdc32e6a6 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for ZHA devices that emit events.""" import voluptuous as vol -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -16,7 +16,7 @@ DEVICE = "device" DEVICE_IEEE = "device_ieee" ZHA_EVENT = "zha_event" -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index ca430368544..5a060bbfaec 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -4,7 +4,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state from homeassistant.const import ( CONF_DEVICE_ID, @@ -24,7 +24,7 @@ from . import DOMAIN # TODO specify your supported trigger types. TRIGGER_TYPES = {"turned_on", "turned_off"} -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), From 6e0aca49affda1101100cc07b3b013e729012dd6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Jun 2021 19:15:01 +0200 Subject: [PATCH 287/750] Replace properties with attr in Axis integration (#51686) --- homeassistant/components/axis/axis_base.py | 35 ++++++------------- .../components/axis/binary_sensor.py | 9 ++--- homeassistant/components/axis/camera.py | 7 ++-- homeassistant/components/axis/light.py | 16 +++------ homeassistant/components/axis/switch.py | 17 ++++----- 5 files changed, 27 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 3e2b1a48eb7..a652aeb6df8 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,5 +1,6 @@ """Base classes for Axis entities.""" +from homeassistant.const import ATTR_IDENTIFIERS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -14,6 +15,8 @@ class AxisEntityBase(Entity): """Initialize the Axis event.""" self.device = device + self._attr_device_info = {ATTR_IDENTIFIERS: {(AXIS_DOMAIN, device.unique_id)}} + async def async_added_to_hass(self): """Subscribe device events.""" self.async_on_remove( @@ -27,11 +30,6 @@ class AxisEntityBase(Entity): """Return True if device is available.""" return self.device.available - @property - def device_info(self): - """Return a device description for device registry.""" - return {"identifiers": {(AXIS_DOMAIN, self.device.unique_id)}} - @callback def update_callback(self, no_delay=None): """Update the entities state.""" @@ -41,11 +39,18 @@ class AxisEntityBase(Entity): class AxisEventBase(AxisEntityBase): """Base common to all Axis entities from event stream.""" + _attr_should_poll = False + def __init__(self, event, device): """Initialize the Axis event.""" super().__init__(device) self.event = event + self._attr_name = f"{device.name} {event.TYPE} {event.id}" + self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" + + self._attr_device_class = event.CLASS + async def async_added_to_hass(self) -> None: """Subscribe sensors events.""" self.event.register_callback(self.update_callback) @@ -54,23 +59,3 @@ class AxisEventBase(AxisEntityBase): async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self.event.remove_callback(self.update_callback) - - @property - def device_class(self): - """Return the class of the event.""" - return self.event.CLASS - - @property - def name(self): - """Return the name of the event.""" - return f"{self.device.name} {self.event.TYPE} {self.event.id}" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def unique_id(self): - """Return a unique identifier for this device.""" - return f"{self.device.unique_id}-{self.event.topic}-{self.event.id}" diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 222a356d4f9..7ef3838b1f7 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -66,6 +66,8 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): super().__init__(event, device) self.cancel_scheduled_update = None + self._attr_device_class = DEVICE_CLASS.get(self.event.CLASS) + @callback def update_callback(self, no_delay=False): """Update the sensor's state, if needed. @@ -126,9 +128,4 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): ): return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" - return super().name - - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS.get(self.event.CLASS) + return self._attr_name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index cf2634b8f3a..bd0cd46a181 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -51,6 +51,8 @@ class AxisCamera(AxisEntityBase, MjpegCamera): } MjpegCamera.__init__(self, config) + self._attr_unique_id = f"{device.unique_id}-camera" + async def async_added_to_hass(self): """Subscribe camera events.""" self.async_on_remove( @@ -71,11 +73,6 @@ class AxisCamera(AxisEntityBase, MjpegCamera): self._mjpeg_url = self.mjpeg_source self._still_image_url = self.image_source - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self.device.unique_id}-camera" - @property def image_source(self) -> str: """Return still image URL for device.""" diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 17b48e7b232..ced795882e1 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -40,6 +40,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AxisLight(AxisEventBase, LightEntity): """Representation of a light Axis event.""" + _attr_should_poll = True + def __init__(self, event, device): """Initialize the Axis light.""" super().__init__(event, device) @@ -49,6 +51,9 @@ class AxisLight(AxisEventBase, LightEntity): self.current_intensity = 0 self.max_intensity = 0 + light_type = device.api.vapix.light_control[self.light_id].light_type + self._attr_name = f"{device.name} {light_type} {event.TYPE} {event.id}" + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} self._attr_color_mode = COLOR_MODE_BRIGHTNESS @@ -68,12 +73,6 @@ class AxisLight(AxisEventBase, LightEntity): ) self.max_intensity = max_intensity["data"]["ranges"][0]["high"] - @property - def name(self): - """Return the name of the light.""" - light_type = self.device.api.vapix.light_control[self.light_id].light_type - return f"{self.device.name} {light_type} {self.event.TYPE} {self.event.id}" - @property def is_on(self): """Return true if light is on.""" @@ -108,8 +107,3 @@ class AxisLight(AxisEventBase, LightEntity): ) ) self.current_intensity = current_intensity["data"]["intensity"] - - @property - def should_poll(self): - """Brightness needs polling.""" - return True diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index e509716fc1f..3a23c3202df 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -30,6 +30,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AxisSwitch(AxisEventBase, SwitchEntity): """Representation of a Axis switch.""" + def __init__(self, event, device): + """Initialize the Axis switch.""" + super().__init__(event, device) + + if event.id and device.api.vapix.ports[event.id].name: + self._attr_name = f"{device.name} {device.api.vapix.ports[event.id].name}" + @property def is_on(self): """Return true if event is active.""" @@ -42,13 +49,3 @@ class AxisSwitch(AxisEventBase, SwitchEntity): async def async_turn_off(self, **kwargs): """Turn off switch.""" await self.device.api.vapix.ports[self.event.id].open() - - @property - def name(self): - """Return the name of the event.""" - if self.event.id and self.device.api.vapix.ports[self.event.id].name: - return ( - f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" - ) - - return super().name From ab490bc7691928b033c238ed22b893adc2f57a45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:23:00 -1000 Subject: [PATCH 288/750] Ensure samsungtv reloads after reauth (#51714) * Ensure samsungtv reloads after reauth - Fixes a case of I/O in the event loop * Ensure config entry is reloaded --- homeassistant/components/samsungtv/config_flow.py | 3 ++- tests/components/samsungtv/test_config_flow.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index a69a456df40..e29298da2eb 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -291,13 +291,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): bridge = SamsungTVBridge.get_bridge( self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] ) - result = bridge.try_connect() + result = await self.hass.async_add_executor_job(bridge.try_connect) if result == RESULT_SUCCESS: new_data = dict(self._reauth_entry.data) new_data[CONF_TOKEN] = bridge.token self.hass.config_entries.async_update_entry( self._reauth_entry, data=new_data ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT): return self.async_abort(reason=result) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5b85ecf7048..1dd11fa5ad9 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -905,6 +905,8 @@ async def test_form_reauth_websocket(hass, remotews: Mock): """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + result = await hass.config_entries.flow.async_init( DOMAIN, context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, @@ -920,6 +922,7 @@ async def test_form_reauth_websocket(hass, remotews: Mock): await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" + assert entry.state == config_entries.ConfigEntryState.LOADED async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): From 4722fdf465b6a50d6977d1200e1e8f2da16226c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:24:30 -1000 Subject: [PATCH 289/750] Fix race condition in samsungtv turn off (#51716) - The state would flip flop if the update happened before the TV had fully shutdown --- .../components/samsungtv/media_player.py | 17 +++++++++++++++-- tests/components/samsungtv/test_media_player.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 5822bafcc55..7efdcdcd439 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_component import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.script import Script @@ -50,6 +51,13 @@ SUPPORT_SAMSUNGTV = ( | SUPPORT_PLAY_MEDIA ) +# Since the TV will take a few seconds to go to sleep +# and actually be seen as off, we need to wait just a bit +# more than the next scan interval +SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta( + seconds=5 +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Samsung TV from a config entry.""" @@ -148,7 +156,12 @@ class SamsungTVDevice(MediaPlayerEntity): """Return the availability of the device.""" if self._auth_failed: return False - return self._state == STATE_ON or self._on_script or self._mac + return ( + self._state == STATE_ON + or self._on_script + or self._mac + or self._power_off_in_progress() + ) @property def device_info(self): @@ -187,7 +200,7 @@ class SamsungTVDevice(MediaPlayerEntity): def turn_off(self): """Turn off media player.""" - self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) + self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME self.send_key("KEY_POWEROFF") # Force closing of remote session to provide instant UI feedback diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 02eceeaacb7..2cdd5cf56df 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -419,6 +419,18 @@ async def test_state_without_turnon(hass, remote): assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) + state = hass.states.get(ENTITY_ID_NOTURNON) + # Should be STATE_UNAVAILABLE after the timer expires + assert state.state == STATE_OFF + + next_update = dt_util.utcnow() + timedelta(seconds=20) + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError, + ), patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_NOTURNON) # Should be STATE_UNAVAILABLE since there is no way to turn it back on assert state.state == STATE_UNAVAILABLE From 453da10b6202b61420a03e79566c5a7885109366 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 10 Jun 2021 19:27:24 +0200 Subject: [PATCH 290/750] Secure not to activate multiple venv in pre_commit hook (#51715) --- script/run-in-env.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/script/run-in-env.sh b/script/run-in-env.sh index 0f531f235b6..271e7a4a034 100755 --- a/script/run-in-env.sh +++ b/script/run-in-env.sh @@ -15,6 +15,7 @@ my_path=$(git rev-parse --show-toplevel) for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then . "${my_path}/${venv}/bin/activate" + break fi done From 9d64b64d348ed4993538be9e8a58eea08483c97d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 10 Jun 2021 19:32:41 +0200 Subject: [PATCH 291/750] Use attrs instead of properties in Airly integration (#51712) --- homeassistant/components/airly/air_quality.py | 20 ++------ homeassistant/components/airly/sensor.py | 47 +++++-------------- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 337d3a723fa..03c1084720d 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -64,18 +64,9 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None: """Initialize.""" super().__init__(coordinator) - self._name = name - self._icon = "mdi:blur" - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon + self._attr_icon = "mdi:blur" + self._attr_name = name + self._attr_unique_id = f"{coordinator.latitude}-{coordinator.longitude}" @property def air_quality_index(self) -> float | None: @@ -97,11 +88,6 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): """Return the attribution.""" return ATTRIBUTION - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return f"{self.coordinator.latitude}-{self.coordinator.longitude}" - @property def device_info(self) -> DeviceInfo: """Return the device info.""" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 562cfec10d5..9c820a02d1b 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,7 +1,7 @@ """Support for the Airly sensor service.""" from __future__ import annotations -from typing import Any, cast +from typing import cast from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -60,18 +60,18 @@ class AirlySensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._name = name - self._description = SENSOR_TYPES[kind] + description = SENSOR_TYPES[kind] + self._attr_device_class = description[ATTR_DEVICE_CLASS] + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_icon = description[ATTR_ICON] + self._attr_name = f"{name} {description[ATTR_LABEL]}" + self._attr_state_class = description[ATTR_STATE_CLASS] + self._attr_unique_id = ( + f"{coordinator.latitude}-{coordinator.longitude}-{kind.lower()}" + ) + self._attr_unit_of_measurement = description[ATTR_UNIT] self.kind = kind self._state = None - self._unit_of_measurement = None - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._attr_state_class = self._description[ATTR_STATE_CLASS] - - @property - def name(self) -> str: - """Return the name.""" - return f"{self._name} {self._description[ATTR_LABEL]}" @property def state(self) -> StateType: @@ -81,26 +81,6 @@ class AirlySensor(CoordinatorEntity, SensorEntity): return round(cast(float, self._state)) return round(cast(float, self._state), 1) - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str | None: - """Return the icon.""" - return self._description[ATTR_ICON] - - @property - def device_class(self) -> str | None: - """Return the device_class.""" - return self._description[ATTR_DEVICE_CLASS] - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" - @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -115,8 +95,3 @@ class AirlySensor(CoordinatorEntity, SensorEntity): "manufacturer": MANUFACTURER, "entry_type": "service", } - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - return self._description[ATTR_UNIT] From 0404acddf92ab226fea458d43afb3b3e09a9cd9a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 10 Jun 2021 20:31:21 +0200 Subject: [PATCH 292/750] Add support for state_class (#51512) --- homeassistant/components/brother/const.py | 25 ++++++++++++++++++++++ homeassistant/components/brother/model.py | 1 + homeassistant/components/brother/sensor.py | 3 ++- tests/components/brother/test_sensor.py | 24 ++++++++++++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 52170057fb1..9e2c096ac76 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Final +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ATTR_ICON, PERCENTAGE from .model import SensorDescription @@ -84,143 +85,167 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_LABEL: ATTR_STATUS.title(), ATTR_UNIT: None, ATTR_ENABLED: True, + ATTR_STATE_CLASS: None, }, ATTR_PAGE_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BW_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_COLOR_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_DUPLEX_COUNTER: { ATTR_ICON: "mdi:file-document-outline", ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(), ATTR_UNIT: UNIT_PAGES, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BLACK_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_CYAN_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_MAGENTA_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_YELLOW_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BELT_UNIT_REMAINING_LIFE: { ATTR_ICON: "mdi:current-ac", ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_FUSER_REMAINING_LIFE: { ATTR_ICON: "mdi:water-outline", ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_LASER_REMAINING_LIFE: { ATTR_ICON: "mdi:spotlight-beam", ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_PF_KIT_1_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_PF_KIT_MP_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_CYAN_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_MAGENTA_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_YELLOW_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_BLACK_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_CYAN_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_MAGENTA_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_YELLOW_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: PERCENTAGE, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ATTR_UPTIME: { ATTR_ICON: None, ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: None, ATTR_ENABLED: False, + ATTR_STATE_CLASS: None, }, } diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py index 22aa95eda50..d53327ae22a 100644 --- a/homeassistant/components/brother/model.py +++ b/homeassistant/components/brother/model.py @@ -11,3 +11,4 @@ class SensorDescription(TypedDict): label: str unit: str | None enabled: bool + state_class: str | None diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 50c9b8d79ff..773da1d09b1 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant @@ -66,6 +66,7 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): self._device_info = device_info self.kind = kind self._attrs: dict[str, Any] = {} + self._attr_state_class = self._description[ATTR_STATE_CLASS] @property def name(self) -> str: diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 225cf5ce87a..b51577b5f3d 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -4,7 +4,11 @@ import json from unittest.mock import Mock, patch from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -51,6 +55,7 @@ async def test_sensors(hass): assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer" assert state.state == "waiting" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.hl_l2340dw_status") assert entry @@ -61,6 +66,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "75" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_black_toner_remaining") assert entry @@ -71,6 +77,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") assert entry @@ -81,6 +88,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "8" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") assert entry @@ -91,6 +99,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "2" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") assert entry @@ -103,6 +112,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 986 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life") assert entry @@ -115,6 +125,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life") assert entry @@ -127,6 +138,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life") assert entry @@ -139,6 +151,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life") assert entry @@ -151,6 +164,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_COUNTER) == 1611 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life") assert entry @@ -161,6 +175,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life") assert entry @@ -171,6 +186,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:current-ac" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life") assert entry @@ -181,6 +197,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "98" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life") assert entry @@ -191,6 +208,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "986" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_page_counter") assert entry @@ -201,6 +219,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "538" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_duplex_unit_pages_counter") assert entry @@ -211,6 +230,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "709" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_b_w_counter") assert entry @@ -221,6 +241,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES assert state.state == "902" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.hl_l2340dw_color_counter") assert entry @@ -232,6 +253,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP assert state.state == "2019-09-24T12:14:56+00:00" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.hl_l2340dw_uptime") assert entry From c937c6d6b51a91d99898380a5b613b7548111a00 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Jun 2021 00:11:06 +0000 Subject: [PATCH 293/750] [ci skip] Translation update --- .../components/ambee/translations/ca.json | 19 +++++++++++++++++++ .../components/ambee/translations/nl.json | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 homeassistant/components/ambee/translations/ca.json create mode 100644 homeassistant/components/ambee/translations/nl.json diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json new file mode 100644 index 00000000000..a3611949083 --- /dev/null +++ b/homeassistant/components/ambee/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Configura Ambee per a integrar-lo amb Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json new file mode 100644 index 00000000000..c8aaf5d9420 --- /dev/null +++ b/homeassistant/components/ambee/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Stel Ambee in om te integreren met Home Assistant." + } + } + } +} \ No newline at end of file From ed9df8393293ea20b97924ccae8959e8a581a5fd Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Fri, 11 Jun 2021 10:06:15 +0300 Subject: [PATCH 294/750] Static typing for no_ip integration (#51694) * no_ip type hints * type import change * change Any to datetime --- .strict-typing | 1 + homeassistant/components/no_ip/__init__.py | 16 ++++++++++++---- mypy.ini | 11 +++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.strict-typing b/.strict-typing index 9a25425d39e..7218823424e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -50,6 +50,7 @@ homeassistant.components.media_player.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.network.* +homeassistant.components.no_ip.* homeassistant.components.notify.* homeassistant.components.number.* homeassistant.components.onewire.* diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 9efaac79562..2e9f5c77fbf 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -1,7 +1,7 @@ """Integrate with NO-IP Dynamic DNS service.""" import asyncio import base64 -from datetime import timedelta +from datetime import datetime, timedelta import logging import aiohttp @@ -10,8 +10,10 @@ import async_timeout import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -51,7 +53,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the NO-IP component.""" domain = config[DOMAIN].get(CONF_DOMAIN) user = config[DOMAIN].get(CONF_USERNAME) @@ -67,7 +69,7 @@ async def async_setup(hass, config): if not result: return False - async def update_domain_interval(now): + async def update_domain_interval(now: datetime) -> None: """Update the NO-IP entry.""" await _update_no_ip(hass, session, domain, auth_str, timeout) @@ -76,7 +78,13 @@ async def async_setup(hass, config): return True -async def _update_no_ip(hass, session, domain, auth_str, timeout): +async def _update_no_ip( + hass: HomeAssistant, + session: aiohttp.ClientSession, + domain: str, + auth_str: bytes, + timeout: int, +) -> bool: """Update NO-IP.""" url = UPDATE_URL diff --git a/mypy.ini b/mypy.ini index 89a4b6fc122..775b0653fe4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -561,6 +561,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.no_ip.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.notify.*] check_untyped_defs = true disallow_incomplete_defs = true From 9e378d51af9402b39a887cd3e9555dbffb218395 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 11 Jun 2021 09:22:47 +0200 Subject: [PATCH 295/750] Reduce modbus schemas and add delay to fan/light (#51664) --- homeassistant/components/modbus/__init__.py | 167 ++++++++------------ 1 file changed, 69 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 242b5e7a0ad..a95dd1fc065 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -114,6 +114,7 @@ BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.strin BASE_COMPONENT_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.positive_int, vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL @@ -122,9 +123,72 @@ BASE_COMPONENT_SCHEMA = vol.Schema( ) +BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + ] + ), + vol.Optional(CONF_COUNT, default=1): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( + [ + DATA_TYPE_INT, + DATA_TYPE_UINT, + DATA_TYPE_FLOAT, + DATA_TYPE_STRING, + DATA_TYPE_CUSTOM, + ] + ), + vol.Optional(CONF_STRUCTURE): cv.string, + vol.Optional(CONF_SCALE, default=1): number_validator, + vol.Optional(CONF_OFFSET, default=0): number_validator, + vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( + [ + CONF_SWAP_NONE, + CONF_SWAP_BYTE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, + ] + ), + } +) + + +BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, + vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, + vol.Optional(CONF_VERIFY): vol.Maybe( + { + vol.Optional(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_INPUT_TYPE): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_COIL, + ] + ), + vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_DELAY, default=0): cv.positive_int, + } + ), + } +) + + CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { - vol.Required(CONF_ADDRESS): cv.positive_int, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( [ CALL_TYPE_REGISTER_HOLDING, @@ -149,7 +213,6 @@ CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { - vol.Required(CONF_ADDRESS): cv.positive_int, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING,): vol.In( [ CALL_TYPE_REGISTER_HOLDING, @@ -169,118 +232,26 @@ COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( } ) -SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( +SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend( { - vol.Required(CONF_ADDRESS): cv.positive_int, vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL] - ), - vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, - vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, - vol.Optional(CONF_VERIFY): vol.Maybe( - { - vol.Optional(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_INPUT_TYPE): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_COIL, - ] - ), - vol.Optional(CONF_STATE_OFF): cv.positive_int, - vol.Optional(CONF_STATE_ON): cv.positive_int, - vol.Optional(CONF_DELAY, default=0): cv.positive_int, - } - ), } ) -LIGHT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL] - ), - vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, - vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, - vol.Optional(CONF_VERIFY): vol.Maybe( - { - vol.Optional(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_INPUT_TYPE): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_COIL, - ] - ), - vol.Optional(CONF_STATE_OFF): cv.positive_int, - vol.Optional(CONF_STATE_ON): cv.positive_int, - } - ), - } -) +LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) -FAN_SCHEMA = BASE_COMPONENT_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_WRITE_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL] - ), - vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, - vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, - vol.Optional(CONF_VERIFY): vol.Maybe( - { - vol.Optional(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_INPUT_TYPE): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_DISCRETE, - CALL_TYPE_REGISTER_INPUT, - CALL_TYPE_COIL, - ] - ), - vol.Optional(CONF_STATE_OFF): cv.positive_int, - vol.Optional(CONF_STATE_ON): cv.positive_int, - } - ), - } -) +FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) -SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( +SENSOR_SCHEMA = BASE_STRUCT_SCHEMA.extend( { - vol.Required(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_COUNT, default=1): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( - [ - DATA_TYPE_INT, - DATA_TYPE_UINT, - DATA_TYPE_FLOAT, - DATA_TYPE_STRING, - DATA_TYPE_CUSTOM, - ] - ), vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_OFFSET, default=0): number_validator, - vol.Optional(CONF_PRECISION, default=0): cv.positive_int, - vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] - ), vol.Optional(CONF_REVERSE_ORDER): cv.boolean, - vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( - [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] - ), - vol.Optional(CONF_SCALE, default=1): number_validator, - vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ) BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { - vol.Required(CONF_ADDRESS): cv.positive_int, vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In( [CALL_TYPE_COIL, CALL_TYPE_DISCRETE] From 49bec86dae526ab14c003c726bc75b4ea07531de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Jun 2021 09:51:12 +0200 Subject: [PATCH 296/750] Add base schema for triggers (#51727) * Add base schema for triggers * Tweak * Make TRIGGER_BASE_SCHEMA a voluptuous schema * Make state trigger BASE_SCHEMA a voluptuous schema --- .../components/device_automation/__init__.py | 2 +- .../components/geo_location/trigger.py | 2 +- .../homeassistant/triggers/event.py | 2 +- .../homeassistant/triggers/homeassistant.py | 3 ++- .../homeassistant/triggers/numeric_state.py | 2 +- .../homeassistant/triggers/state.py | 22 +++++++++---------- .../components/homeassistant/triggers/time.py | 2 +- .../homeassistant/triggers/time_pattern.py | 2 +- homeassistant/components/litejet/trigger.py | 2 +- homeassistant/components/mqtt/trigger.py | 2 +- homeassistant/components/sun/trigger.py | 2 +- homeassistant/components/tag/trigger.py | 2 +- homeassistant/components/template/trigger.py | 2 +- homeassistant/components/webhook/trigger.py | 7 ++++-- homeassistant/components/zone/trigger.py | 2 +- homeassistant/helpers/config_validation.py | 4 +++- 16 files changed, 33 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index d6317ce45ce..93b0b9a4a9d 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -25,7 +25,7 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig DOMAIN = "device_automation" -DEVICE_TRIGGER_BASE_SCHEMA = vol.Schema( +DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str, diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 4410d39c0a6..90621e7062c 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -18,7 +18,7 @@ EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "geo_location", vol.Required(CONF_SOURCE): cv.string, diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 2e78a93315d..ec44c861835 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv, template CONF_EVENT_TYPE = "event_type" CONF_EVENT_CONTEXT = "context" -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "event", vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]), diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 2f3ae8e6ad2..3593e27b530 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -3,13 +3,14 @@ import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HassJob, callback +from homeassistant.helpers import config_validation as cv # mypy: allow-untyped-defs EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "homeassistant", vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN), diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 05eed9ee27b..4ab92c36205 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -44,7 +44,7 @@ def validate_above_below(value): TRIGGER_SCHEMA = vol.All( - vol.Schema( + cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "numeric_state", vol.Required(CONF_ENTITY_ID): cv.entity_ids, diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 69cddbfe126..edcf6b09a78 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -27,25 +27,25 @@ CONF_ENTITY_ID = "entity_id" CONF_FROM = "from" CONF_TO = "to" -BASE_SCHEMA = { - vol.Required(CONF_PLATFORM): "state", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_FOR): cv.positive_time_period_template, - vol.Optional(CONF_ATTRIBUTE): cv.match_all, -} - -TRIGGER_STATE_SCHEMA = vol.Schema( +BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): "state", + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_FOR): cv.positive_time_period_template, + vol.Optional(CONF_ATTRIBUTE): cv.match_all, + } +) + +TRIGGER_STATE_SCHEMA = BASE_SCHEMA.extend( { - **BASE_SCHEMA, # These are str on purpose. Want to catch YAML conversions vol.Optional(CONF_FROM): vol.Any(str, [str]), vol.Optional(CONF_TO): vol.Any(str, [str]), } ) -TRIGGER_ATTRIBUTE_SCHEMA = vol.Schema( +TRIGGER_ATTRIBUTE_SCHEMA = BASE_SCHEMA.extend( { - **BASE_SCHEMA, vol.Optional(CONF_FROM): cv.match_all, vol.Optional(CONF_TO): cv.match_all, } diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 6668672732e..3d7c612cc55 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -29,7 +29,7 @@ _TIME_TRIGGER_SCHEMA = vol.Any( msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'", ) -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "time", vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]), diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 859f76b773b..bd56ea89663 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -43,7 +43,7 @@ class TimePattern: TRIGGER_SCHEMA = vol.All( - vol.Schema( + cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "time_pattern", CONF_HOURS: TimePattern(maximum=23), diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 6800282766b..5bbd8e2f912 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -15,7 +15,7 @@ CONF_NUMBER = "number" CONF_HELD_MORE_THAN = "held_more_than" CONF_HELD_LESS_THAN = "held_less_than" -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "litejet", vol.Required(CONF_NUMBER): cv.positive_int, diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 34c47aec791..ae184c9182a 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -19,7 +19,7 @@ CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" DEFAULT_QOS = 0 -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): mqtt.DOMAIN, vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic_template, diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index d2b6f6de560..2a46f665e3c 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -15,7 +15,7 @@ from homeassistant.helpers.event import async_track_sunrise, async_track_sunset # mypy: allow-untyped-defs, no-check-untyped-defs -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "sun", vol.Required(CONF_EVENT): cv.sun_event, diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index 4f6dd89a252..e46e737986e 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -7,7 +7,7 @@ from homeassistant.helpers import config_validation as cv from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(TAG_ID): vol.All(cv.ensure_list, [cv.string]), diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 998984e0b9a..c4751055962 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -18,7 +18,7 @@ from homeassistant.helpers.template import result_as_boolean _LOGGER = logging.getLogger(__name__) -TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "template", vol.Required(CONF_VALUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index a82dd0251c9..687a72108da 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -12,8 +12,11 @@ import homeassistant.helpers.config_validation as cv DEPENDENCIES = ("webhook",) -TRIGGER_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): "webhook", vol.Required(CONF_WEBHOOK_ID): cv.string} +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): "webhook", + vol.Required(CONF_WEBHOOK_ID): cv.string, + } ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index db5ca2cf01b..64c904defea 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -21,7 +21,7 @@ DEFAULT_EVENT = EVENT_ENTER _EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"} -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "zone", vol.Required(CONF_ENTITY_ID): cv.entity_ids, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 324173ed2e8..feb03cf04a2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1106,8 +1106,10 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema( ) ) +TRIGGER_BASE_SCHEMA = vol.Schema({vol.Required(CONF_PLATFORM): str}) + TRIGGER_SCHEMA = vol.All( - ensure_list, [vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA)] + ensure_list, [TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)] ) _SCRIPT_DELAY_SCHEMA = vol.Schema( From ba6b527d6132294378c914c68360ec60a4eb446b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Jun 2021 11:13:55 +0200 Subject: [PATCH 297/750] Improve editing of device actions referencing non-added HVAC (#51706) * Improve editing of device actions referencing non-added HVAC * Improve test coverage --- .../components/climate/device_action.py | 30 ++- homeassistant/helpers/entity.py | 17 ++ .../components/climate/test_device_action.py | 227 ++++++++++++------ 3 files changed, 194 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index de46eae0d7b..6afc4d294cb 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -5,15 +5,15 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_capability, get_supported_features from . import DOMAIN, const @@ -48,11 +48,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - - # We need a state or else we can't populate the HVAC and preset modes. - if state is None: - continue + supported_features = get_supported_features(hass, entry.entity_id) base_action = { CONF_DEVICE_ID: device_id, @@ -61,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: } actions.append({**base_action, CONF_TYPE: "set_hvac_mode"}) - if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE: + if supported_features & const.SUPPORT_PRESET_MODE: actions.append({**base_action, CONF_TYPE: "set_preset_mode"}) return actions @@ -87,18 +83,26 @@ async def async_call_action_from_config( async def async_get_action_capabilities(hass, config): """List action capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) action_type = config[CONF_TYPE] fields = {} if action_type == "set_hvac_mode": - hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + try: + hvac_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_HVAC_MODES) + or [] + ) + except HomeAssistantError: + hvac_modes = [] fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) elif action_type == "set_preset_mode": - if state: - preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) - else: + try: + preset_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_PRESET_MODES) + or [] + ) + except HomeAssistantError: preset_modes = [] fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dce68c80871..e9d1e1d2e07 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -93,6 +93,23 @@ def async_generate_entity_id( return test_string +def get_capability(hass: HomeAssistant, entity_id: str, capability: str) -> str | None: + """Get a capability attribute of an entity. + + First try the statemachine, then entity registry. + """ + state = hass.states.get(entity_id) + if state: + return state.attributes.get(capability) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(entity_id) + if not entry: + raise HomeAssistantError(f"Unknown entity {entity_id}") + + return entry.capabilities.get(capability) if entry.capabilities else None + + def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None: """Get device class of an entity. diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index dc956f0738c..fd5c5548916 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -30,7 +30,24 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_action_types", + [ + (False, 0, 0, ["set_hvac_mode"]), + (False, const.SUPPORT_PRESET_MODE, 0, ["set_hvac_mode", "set_preset_mode"]), + (True, 0, 0, ["set_hvac_mode"]), + (True, 0, const.SUPPORT_PRESET_MODE, ["set_hvac_mode", "set_preset_mode"]), + ], +) +async def test_get_actions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_action_types, +): """Test we get the expected actions from a climate.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -38,46 +55,30 @@ async def test_get_actions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) - hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) - expected_actions = [ - { - "domain": DOMAIN, - "type": "set_hvac_mode", - "device_id": device_entry.id, - "entity_id": "climate.test_5678", - }, - { - "domain": DOMAIN, - "type": "set_preset_mode", - "device_id": device_entry.id, - "entity_id": "climate.test_5678", - }, - ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_action_hvac_only(hass, device_reg, entity_reg): - """Test we get the expected actions from a climate.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) - hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) - expected_actions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + + expected_actions = [] + + expected_actions += [ { "domain": DOMAIN, - "type": "set_hvac_mode", + "type": action, "device_id": device_entry.id, - "entity_id": "climate.test_5678", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in expected_action_types ] + actions = await async_get_device_automations(hass, "action", device_entry.id) assert_lists_same(actions, expected_actions) @@ -142,61 +143,153 @@ async def test_action(hass): assert len(set_preset_mode_calls) == 1 -async def test_capabilities(hass): +@pytest.mark.parametrize( + "set_state,capabilities_reg,capabilities_state,action,expected_capabilities", + [ + ( + False, + {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {}, + "set_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + {}, + "set_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + "set_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + "set_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ], +) +async def test_capabilities( + hass, + device_reg, + entity_reg, + set_state, + capabilities_reg, + capabilities_state, + action, + expected_capabilities, +): """Test getting capabilities.""" - hass.states.async_set( - "climate.entity", - const.HVAC_MODE_COOL, - { - const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], - const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], - }, + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + capabilities=capabilities_reg, + ) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + capabilities_state, + ) - # Set HVAC mode capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "climate.entity", - "type": "set_hvac_mode", + "entity_id": f"{DOMAIN}.test_5678", + "type": action, }, ) assert capabilities and "extra_fields" in capabilities - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ - { - "name": "hvac_mode", - "options": [("cool", "cool"), ("off", "off")], - "required": True, - "type": "select", - } - ] + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) + + +@pytest.mark.parametrize( + "action,capability_name", + [("set_hvac_mode", "hvac_mode"), ("set_preset_mode", "preset_mode")], +) +async def test_capabilities_mising_entity( + hass, device_reg, entity_reg, action, capability_name +): + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) - # Set preset mode capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "climate.entity", - "type": "set_preset_mode", + "entity_id": f"{DOMAIN}.test_5678", + "type": action, }, ) - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ + expected_capabilities = [ { - "name": "preset_mode", - "options": [("home", "home"), ("away", "away")], + "name": capability_name, + "options": [], "required": True, "type": "select", } ] + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) From 7393cba0a5808eaaa6503294cda861bc9b38223f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Jun 2021 11:36:54 +0200 Subject: [PATCH 298/750] Mock WLED in all WLED tests (#51724) * Mock WLED in all WLED tests * Update tests/components/wled/conftest.py Co-authored-by: Erik Montnemery * Remove useless AsyncMock * Add missing asserts Co-authored-by: Erik Montnemery --- tests/components/wled/__init__.py | 57 -- tests/components/wled/conftest.py | 79 ++- tests/components/wled/test_config_flow.py | 69 +- tests/components/wled/test_init.py | 57 +- tests/components/wled/test_light.py | 777 +++++++++++----------- tests/components/wled/test_sensor.py | 17 +- tests/components/wled/test_switch.py | 184 ++--- 7 files changed, 612 insertions(+), 628 deletions(-) diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py index a39d1ef6453..40723f54294 100644 --- a/tests/components/wled/__init__.py +++ b/tests/components/wled/__init__.py @@ -1,58 +1 @@ """Tests for the WLED integration.""" - -import json - -from homeassistant.components.wled.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry, load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def init_integration( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - rgbw: bool = False, - skip_setup: bool = False, -) -> MockConfigEntry: - """Set up the WLED integration in Home Assistant.""" - - fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json" - data = json.loads(load_fixture(fixture)) - - aioclient_mock.get( - "http://192.168.1.123:80/json/", - json=data, - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.post( - "http://192.168.1.123:80/json/state", - json=data["state"], - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "http://192.168.1.123:80/json/info", - json=data["info"], - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "http://192.168.1.123:80/json/state", - json=data["state"], - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"} - ) - - entry.add_to_hass(hass) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index 7b8eb9cd50c..f66171b6025 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -1,2 +1,79 @@ -"""wled conftest.""" +"""Fixtures for WLED integration tests.""" +import json +from typing import Generator +from unittest.mock import MagicMock, patch + +import pytest +from wled import Device as WLEDDevice + +from homeassistant.components.wled.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture from tests.components.light.conftest import mock_light_profiles # noqa: F401 + + +@pytest.fixture(autouse=True) +async def mock_persistent_notification(hass: HomeAssistant) -> None: + """Set up component for persistent notifications.""" + await async_setup_component(hass, "persistent_notification", {}) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"}, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.wled.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_wled_config_flow( + request: pytest.FixtureRequest, +) -> Generator[None, MagicMock, None]: + """Return a mocked WLED client.""" + with patch( + "homeassistant.components.wled.config_flow.WLED", autospec=True + ) as wled_mock: + wled = wled_mock.return_value + wled.update.return_value = WLEDDevice(json.loads(load_fixture("wled/rgb.json"))) + yield wled + + +@pytest.fixture +def mock_wled(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked WLED client.""" + fixture: str = "wled/rgb.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + device = WLEDDevice(json.loads(load_fixture(fixture))) + with patch( + "homeassistant.components.wled.coordinator.WLED", autospec=True + ) as wled_mock: + wled = wled_mock.return_value + wled.update.return_value = device + yield wled + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock +) -> MockConfigEntry: + """Set up the WLED integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 358902a8821..ea0167ed6d5 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the WLED config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock -import aiohttp from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -14,22 +13,13 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import init_integration - -from tests.common import load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None ) -> None: """Test the full manual user flow from start to finish.""" - aioclient_mock.get( - "http://192.168.1.123:80/json/", - text=load_fixture("wled/rgb.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -51,15 +41,9 @@ async def test_full_user_flow_implementation( async def test_full_zeroconf_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None ) -> None: """Test the full manual user flow from start to finish.""" - aioclient_mock.get( - "http://192.168.1.123:80/json/", - text=load_fixture("wled/rgb.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -91,16 +75,11 @@ async def test_full_zeroconf_flow_implementation( assert result2["data"][CONF_MAC] == "aabbccddeeff" -@patch( - "homeassistant.components.wled.coordinator.WLED.update", - side_effect=WLEDConnectionError, -) async def test_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_wled_config_flow: MagicMock ) -> None: """Test we show user form on WLED connection error.""" - aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) - + mock_wled_config_flow.update.side_effect = WLEDConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -112,15 +91,11 @@ async def test_connection_error( assert result.get("errors") == {"base": "cannot_connect"} -@patch( - "homeassistant.components.wled.coordinator.WLED.update", - side_effect=WLEDConnectionError, -) async def test_zeroconf_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_wled_config_flow: MagicMock ) -> None: """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) + mock_wled_config_flow.update.side_effect = WLEDConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -132,15 +107,11 @@ async def test_zeroconf_connection_error( assert result.get("reason") == "cannot_connect" -@patch( - "homeassistant.components.wled.coordinator.WLED.update", - side_effect=WLEDConnectionError, -) async def test_zeroconf_confirm_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_wled_config_flow: MagicMock ) -> None: """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) + mock_wled_config_flow.update.side_effect = WLEDConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -157,11 +128,11 @@ async def test_zeroconf_confirm_connection_error( async def test_user_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MagicMock, + mock_wled_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -173,11 +144,11 @@ async def test_user_device_exists_abort( async def test_zeroconf_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MagicMock, + mock_wled_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -189,11 +160,11 @@ async def test_zeroconf_device_exists_abort( async def test_zeroconf_with_mac_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled_config_flow: MagicMock, ) -> None: """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 52b014760e5..50ee5520e5d 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,5 +1,5 @@ """Tests for the WLED integration.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from wled import WLEDConnectionError @@ -7,37 +7,44 @@ from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.components.wled import init_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry -@patch( - "homeassistant.components.wled.coordinator.WLED.update", - side_effect=WLEDConnectionError, -) -async def test_config_entry_not_ready( - mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the WLED configuration entry not ready.""" - entry = await init_integration(hass, aioclient_mock) - assert entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_unload_config_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: AsyncMock ) -> None: """Test the WLED configuration entry unloading.""" - entry = await init_integration(hass, aioclient_mock) - assert hass.data[DOMAIN] - - await hass.config_entries.async_unload(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) -async def test_setting_unique_id(hass, aioclient_mock): - """Test we set unique ID if not set yet.""" - entry = await init_integration(hass, aioclient_mock) +@patch( + "homeassistant.components.wled.coordinator.WLED.request", + side_effect=WLEDConnectionError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the WLED configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setting_unique_id( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test we set unique ID if not set yet.""" assert hass.data[DOMAIN] - assert entry.unique_id == "aabbccddeeff" + assert init_integration.unique_id == "aabbccddeeff" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 721c0cc3880..666006b07b1 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,8 +1,9 @@ """Tests for the WLED light platform.""" import json -from unittest.mock import patch +from unittest.mock import MagicMock -from wled import Device as WLEDDevice, WLEDConnectionError +import pytest +from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -38,17 +39,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, load_fixture -from tests.components.wled import init_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture async def test_rgb_light_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: """Test the creation and values of the WLED lights.""" - await init_integration(hass, aioclient_mock) - entity_registry = er.async_get(hass) # First segment of the strip @@ -101,143 +98,142 @@ async def test_rgb_light_state( async def test_segment_change_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the change of state of the WLED segments.""" - await init_integration(hass, aioclient_mock) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + on=False, + segment_id=0, + transition=50, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - on=False, - segment_id=0, - transition=50, - ) - - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_BRIGHTNESS: 42, - ATTR_EFFECT: "Chase", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_RGB_COLOR: [255, 0, 0], - ATTR_TRANSITION: 5, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - brightness=42, - color_primary=(255, 0, 0), - effect="Chase", - on=True, - segment_id=0, - transition=50, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_EFFECT: "Chase", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_RGB_COLOR: [255, 0, 0], + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 2 + mock_wled.segment.assert_called_with( + brightness=42, + color_primary=(255, 0, 0), + effect="Chase", + on=True, + segment_id=0, + transition=50, + ) async def test_master_change_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the change of state of the WLED master light control.""" - await init_integration(hass, aioclient_mock) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.master.call_count == 1 + mock_wled.master.assert_called_with( + on=False, + transition=50, + ) - with patch("wled.WLED.master") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - on=False, - transition=50, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_ENTITY_ID: "light.wled_rgb_light_master", + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.master.call_count == 2 + mock_wled.master.assert_called_with( + brightness=42, + on=True, + transition=50, + ) - with patch("wled.WLED.master") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_BRIGHTNESS: 42, - ATTR_ENTITY_ID: "light.wled_rgb_light_master", - ATTR_TRANSITION: 5, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - brightness=42, - on=True, - transition=50, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.master.call_count == 3 + mock_wled.master.assert_called_with( + on=False, + transition=50, + ) - with patch("wled.WLED.master") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - on=False, - transition=50, - ) - - with patch("wled.WLED.master") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_BRIGHTNESS: 42, - ATTR_ENTITY_ID: "light.wled_rgb_light_master", - ATTR_TRANSITION: 5, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - brightness=42, - on=True, - transition=50, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_ENTITY_ID: "light.wled_rgb_light_master", + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.master.call_count == 4 + mock_wled.master.assert_called_with( + brightness=42, + on=True, + transition=50, + ) async def test_dynamically_handle_segments( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" - await init_integration(hass, aioclient_mock) - assert hass.states.get("light.wled_rgb_light_master") assert hass.states.get("light.wled_rgb_light_segment_0") assert hass.states.get("light.wled_rgb_light_segment_1") - data = json.loads(load_fixture("wled/rgb_single_segment.json")) - device = WLEDDevice(data) + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb_single_segment.json")) + ) - # Test removal if segment went missing, including the master entity - with patch( - "homeassistant.components.wled.coordinator.WLED.update", - return_value=device, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() - assert hass.states.get("light.wled_rgb_light_segment_0") - assert not hass.states.get("light.wled_rgb_light_segment_1") - assert not hass.states.get("light.wled_rgb_light_master") + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("light.wled_rgb_light_segment_0") + assert not hass.states.get("light.wled_rgb_light_segment_1") + assert not hass.states.get("light.wled_rgb_light_master") # Test adding if segment shows up again, including the master entity + mock_wled.update.return_value = return_value async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -246,345 +242,336 @@ async def test_dynamically_handle_segments( assert hass.states.get("light.wled_rgb_light_segment_1") +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) async def test_single_segment_behavior( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the behavior of the integration with a single segment.""" - await init_integration(hass, aioclient_mock) + device = mock_wled.update.return_value - data = json.loads(load_fixture("wled/rgb_single_segment.json")) - device = WLEDDevice(data) - - # Test absent master - with patch( - "homeassistant.components.wled.coordinator.WLED.update", - return_value=device, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() - - assert not hass.states.get("light.wled_rgb_light_master") - - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state - assert state.state == STATE_ON + assert not hass.states.get("light.wled_rgb_light_master") + state = hass.states.get("light.wled_rgb_light") + assert state + assert state.state == STATE_ON # Test segment brightness takes master into account device.state.brightness = 100 device.state.segments[0].brightness = 255 - with patch( - "homeassistant.components.wled.coordinator.WLED.update", - return_value=device, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state - assert state.attributes.get(ATTR_BRIGHTNESS) == 100 + state = hass.states.get("light.wled_rgb_light") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 100 # Test segment is off when master is off device.state.on = False - with patch( - "homeassistant.components.wled.coordinator.WLED.update", - return_value=device, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state - assert state.state == STATE_OFF + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state + assert state.state == STATE_OFF # Test master is turned off when turning off a single segment - with patch("wled.WLED.master") as master_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, - blocking=True, - ) - await hass.async_block_till_done() - master_mock.assert_called_once_with( - on=False, - transition=50, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.master.call_count == 1 + mock_wled.master.assert_called_with( + on=False, + transition=50, + ) # Test master is turned on when turning on a single segment, and segment # brightness is set to 255. - with patch("wled.WLED.master") as master_mock, patch( - "wled.WLED.segment" - ) as segment_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_TRANSITION: 5, - ATTR_BRIGHTNESS: 42, - }, - blocking=True, - ) - await hass.async_block_till_done() - master_mock.assert_called_once_with(on=True, transition=50, brightness=42) - segment_mock.assert_called_once_with(on=True, segment_id=0, brightness=255) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + assert mock_wled.master.call_count == 2 + mock_wled.segment.assert_called_with(on=True, segment_id=0, brightness=255) + mock_wled.master.assert_called_with(on=True, transition=50, brightness=42) async def test_light_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED lights.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) - await init_integration(hass, aioclient_mock) + mock_wled.segment.side_effect = WLEDError - with patch("homeassistant.components.wled.coordinator.WLED.update"): - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(on=False, segment_id=0) async def test_light_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" - await init_integration(hass, aioclient_mock) + mock_wled.segment.side_effect = WLEDConnectionError - with patch("homeassistant.components.wled.coordinator.WLED.update"), patch( - "homeassistant.components.wled.coordinator.WLED.segment", - side_effect=WLEDConnectionError, - ): - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(on=False, segment_id=0) +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) async def test_rgbw_light( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: """Test RGBW support for WLED.""" - await init_integration(hass, aioclient_mock, rgbw=True) - state = hass.states.get("light.wled_rgbw_light") + assert state assert state.state == STATE_ON assert state.attributes.get(ATTR_RGBW_COLOR) == (255, 0, 0, 139) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.wled_rgbw_light", - ATTR_RGBW_COLOR: (255, 255, 255, 255), - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - color_primary=(255, 255, 255, 255), - on=True, - segment_id=0, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.wled_rgbw_light", + ATTR_RGBW_COLOR: (255, 255, 255, 255), + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + color_primary=(255, 255, 255, 255), + on=True, + segment_id=0, + ) async def test_effect_service( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: """Test the effect service of a WLED light.""" - await init_integration(hass, aioclient_mock) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_INTENSITY: 200, + ATTR_PALETTE: "Tiamat", + ATTR_REVERSE: True, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + effect="Rainbow", + intensity=200, + palette="Tiamat", + reverse=True, + segment_id=0, + speed=100, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_INTENSITY: 200, - ATTR_PALETTE: "Tiamat", - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - effect="Rainbow", - intensity=200, - palette="Tiamat", - reverse=True, - segment_id=0, - speed=100, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 2 + mock_wled.segment.assert_called_with( + segment_id=0, + effect=9, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - segment_id=0, - effect=9, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_INTENSITY: 200, + ATTR_REVERSE: True, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 3 + mock_wled.segment.assert_called_with( + intensity=200, + reverse=True, + segment_id=0, + speed=100, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_INTENSITY: 200, - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - intensity=200, - reverse=True, - segment_id=0, - speed=100, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_PALETTE: "Tiamat", + ATTR_REVERSE: True, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 4 + mock_wled.segment.assert_called_with( + effect="Rainbow", + palette="Tiamat", + reverse=True, + segment_id=0, + speed=100, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_PALETTE: "Tiamat", - ATTR_REVERSE: True, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - effect="Rainbow", - palette="Tiamat", - reverse=True, - segment_id=0, - speed=100, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_INTENSITY: 200, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 5 + mock_wled.segment.assert_called_with( + effect="Rainbow", + intensity=200, + segment_id=0, + speed=100, + ) - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_INTENSITY: 200, - ATTR_SPEED: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - effect="Rainbow", - intensity=200, - segment_id=0, - speed=100, - ) - - with patch("wled.WLED.segment") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - { - ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_INTENSITY: 200, - ATTR_REVERSE: True, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - effect="Rainbow", - intensity=200, - reverse=True, - segment_id=0, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_INTENSITY: 200, + ATTR_REVERSE: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 6 + mock_wled.segment.assert_called_with( + effect="Rainbow", + intensity=200, + reverse=True, + segment_id=0, + ) async def test_effect_service_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED effect service.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) - await init_integration(hass, aioclient_mock) + mock_wled.segment.side_effect = WLEDError - with patch("homeassistant.components.wled.coordinator.WLED.update"): - await hass.services.async_call( - DOMAIN, - SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(effect=9, segment_id=0) async def test_preset_service( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: """Test the preset service of a WLED light.""" - await init_integration(hass, aioclient_mock) - - with patch("wled.WLED.preset") as light_mock: - await hass.services.async_call( - DOMAIN, - SERVICE_PRESET, - { - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", - ATTR_PRESET: 1, - }, - blocking=True, - ) - await hass.async_block_till_done() - light_mock.assert_called_once_with( - preset=1, - ) + await hass.services.async_call( + DOMAIN, + SERVICE_PRESET, + { + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_PRESET: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset=1) async def test_preset_service_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED preset service.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) - await init_integration(hass, aioclient_mock) + mock_wled.preset.side_effect = WLEDError - with patch("homeassistant.components.wled.coordinator.WLED.update"): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESET, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_PRESET: 1}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_PRESET, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_PRESET: 1}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") - assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset=1) diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index fcd36dd70a9..635cf47c687 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -28,16 +28,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from tests.components.wled import init_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry async def test_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the creation and values of the WLED sensors.""" - - entry = await init_integration(hass, aioclient_mock, skip_setup=True) registry = er.async_get(hass) # Pre-create registry entries for disabled by default sensors @@ -90,9 +89,10 @@ async def test_sensors( ) # Setup + mock_config_entry.add_to_hass(hass) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.wled_rgb_light_estimated_current") @@ -184,10 +184,9 @@ async def test_sensors( ), ) async def test_disabled_by_default_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_id: str + hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str ) -> None: """Test the disabled by default WLED sensors.""" - await init_integration(hass, aioclient_mock) registry = er.async_get(hass) state = hass.states.get(entity_id) diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 7a396092ff9..eb8b8b526e0 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,7 +1,8 @@ """Tests for the WLED switch platform.""" -from unittest.mock import patch +from unittest.mock import MagicMock -from wled import WLEDConnectionError +import pytest +from wled import WLEDConnectionError, WLEDError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -22,16 +23,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.components.wled import init_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry async def test_switch_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: """Test the creation and values of the WLED switches.""" - await init_integration(hass, aioclient_mock) - entity_registry = er.async_get(hass) state = hass.states.get("switch.wled_rgb_light_nightlight") @@ -68,113 +66,115 @@ async def test_switch_state( async def test_switch_change_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock ) -> None: """Test the change of state of the WLED switches.""" - await init_integration(hass, aioclient_mock) # Nightlight - with patch("wled.WLED.nightlight") as nightlight_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() - nightlight_mock.assert_called_once_with(on=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.nightlight.call_count == 1 + mock_wled.nightlight.assert_called_with(on=True) - with patch("wled.WLED.nightlight") as nightlight_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() - nightlight_mock.assert_called_once_with(on=False) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.nightlight.call_count == 2 + mock_wled.nightlight.assert_called_with(on=False) # Sync send - with patch("wled.WLED.sync") as sync_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, - blocking=True, - ) - await hass.async_block_till_done() - sync_mock.assert_called_once_with(send=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.sync.call_count == 1 + mock_wled.sync.assert_called_with(send=True) - with patch("wled.WLED.sync") as sync_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, - blocking=True, - ) - await hass.async_block_till_done() - sync_mock.assert_called_once_with(send=False) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.sync.call_count == 2 + mock_wled.sync.assert_called_with(send=False) # Sync receive - with patch("wled.WLED.sync") as sync_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, - blocking=True, - ) - await hass.async_block_till_done() - sync_mock.assert_called_once_with(receive=False) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.sync.call_count == 3 + mock_wled.sync.assert_called_with(receive=False) - with patch("wled.WLED.sync") as sync_mock: - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, - blocking=True, - ) - await hass.async_block_till_done() - sync_mock.assert_called_once_with(receive=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.sync.call_count == 4 + mock_wled.sync.assert_called_with(receive=True) async def test_switch_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) - await init_integration(hass, aioclient_mock) + mock_wled.nightlight.side_effect = WLEDError - with patch("homeassistant.components.wled.coordinator.WLED.update"): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_OFF - assert "Invalid response from API" in caplog.text + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state + assert state.state == STATE_OFF + assert "Invalid response from API" in caplog.text async def test_switch_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" - await init_integration(hass, aioclient_mock) + mock_wled.nightlight.side_effect = WLEDConnectionError - with patch("homeassistant.components.wled.coordinator.WLED.update"), patch( - "homeassistant.components.wled.coordinator.WLED.nightlight", - side_effect=WLEDConnectionError, - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text From 2a51587bc3bdb07af9ab230950b08dcf91ae772f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 11 Jun 2021 11:41:41 +0200 Subject: [PATCH 299/750] Remove reverse_order (replaced by generic swap) (#51665) Remove reverse_order (replaced by generic swap). --- homeassistant/components/modbus/__init__.py | 15 +++++++++------ homeassistant/components/modbus/validators.py | 9 --------- tests/components/modbus/test_init.py | 8 -------- tests/components/modbus/test_sensor.py | 12 ------------ 4 files changed, 9 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a95dd1fc065..382fe63ab69 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -242,12 +242,15 @@ LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) -SENSOR_SCHEMA = BASE_STRUCT_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_REVERSE_ORDER): cv.boolean, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } +SENSOR_SCHEMA = vol.All( + cv.deprecated(CONF_REVERSE_ORDER), + BASE_STRUCT_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_REVERSE_ORDER): cv.boolean, + } + ), ) BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 1b94010b5ef..062be57f51c 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -17,11 +17,9 @@ from homeassistant.const import ( from .const import ( CONF_DATA_TYPE, - CONF_REVERSE_ORDER, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_NONE, - CONF_SWAP_WORD, DATA_TYPE_CUSTOM, DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, @@ -71,13 +69,6 @@ def sensor_schema_validator(config): swap_type = config.get(CONF_SWAP) - if CONF_REVERSE_ORDER in config: - if config[CONF_REVERSE_ORDER]: - swap_type = CONF_SWAP_WORD - else: - swap_type = CONF_SWAP_NONE - del config[CONF_REVERSE_ORDER] - if config.get(CONF_SWAP) != CONF_SWAP_NONE: if swap_type == CONF_SWAP_BYTE: regs_needed = 1 diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 737c5ef2bb6..435b8446b6b 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -41,7 +41,6 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, CONF_PARITY, - CONF_REVERSE_ORDER, CONF_STOPBITS, CONF_SWAP, CONF_SWAP_BYTE, @@ -136,13 +135,6 @@ async def test_number_validator(): CONF_NAME: TEST_SENSOR_NAME, CONF_COUNT: 2, CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_REVERSE_ORDER: True, - }, - { - CONF_NAME: TEST_SENSOR_NAME, - CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, - CONF_REVERSE_ORDER: False, }, { CONF_NAME: TEST_SENSOR_NAME, diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 3de6bd2c172..4deb5ee8392 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_PRECISION, CONF_REGISTERS, - CONF_REVERSE_ORDER, CONF_SCALE, CONF_SWAP, CONF_SWAP_BYTE, @@ -55,7 +54,6 @@ from tests.common import mock_restore_cache CONF_DATA_TYPE: "int", CONF_PRECISION: 0, CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, CONF_OFFSET: 0, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DEVICE_CLASS: "battery", @@ -67,7 +65,6 @@ from tests.common import mock_restore_cache CONF_DATA_TYPE: "int", CONF_PRECISION: 0, CONF_SCALE: 1, - CONF_REVERSE_ORDER: False, CONF_OFFSET: 0, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, CONF_DEVICE_CLASS: "battery", @@ -331,15 +328,6 @@ async def test_config_wrong_struct_sensor( [0x89AB, 0xCDEF], str(0x89ABCDEF), ), - ( - { - CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_UINT, - CONF_REVERSE_ORDER: True, - }, - [0x89AB, 0xCDEF], - str(0xCDEF89AB), - ), ( { CONF_COUNT: 4, From b4aeddd12f310efa8dec1ec3ae85636e0909ec9d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Jun 2021 12:45:22 +0200 Subject: [PATCH 300/750] Add 100% test coverage to WLED integration (#51743) --- tests/components/wled/test_light.py | 13 +++++++++ tests/components/wled/test_sensor.py | 42 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 666006b07b1..c37f28a882e 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -551,6 +551,19 @@ async def test_preset_service( assert mock_wled.preset.call_count == 1 mock_wled.preset.assert_called_with(preset=1) + await hass.services.async_call( + DOMAIN, + SERVICE_PRESET, + { + ATTR_ENTITY_ID: "light.wled_rgb_light_master", + ATTR_PRESET: 2, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.preset.call_count == 2 + mock_wled.preset.assert_called_with(preset=2) + async def test_preset_service_error( hass: HomeAssistant, diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 635cf47c687..4f2b07f4f51 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( DATA_BYTES, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -196,3 +197,44 @@ async def test_disabled_by_default_sensors( assert entry assert entry.disabled assert entry.disabled_by == er.DISABLED_INTEGRATION + + +@pytest.mark.parametrize( + "key", + [ + "bssid", + "channel", + "rssi", + "signal", + ], +) +async def test_no_wifi_support( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_wled: MagicMock, + key: str, +) -> None: + """Test missing Wi-Fi information from WLED device.""" + registry = er.async_get(hass) + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"aabbccddeeff_wifi_{key}", + suggested_object_id=f"wled_rgb_light_wifi_{key}", + disabled_by=None, + ) + + # Remove Wi-Fi info + device = mock_wled.update.return_value + device.info.wifi = None + + # Setup + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"sensor.wled_rgb_light_wifi_{key}") + assert state + assert state.state == STATE_UNKNOWN From f17a5f0db93d696442d9144e9064089d37e088db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Jun 2021 13:29:50 +0200 Subject: [PATCH 301/750] Clean up redudant exceptions from handlers (#51741) --- homeassistant/components/coolmaster/__init__.py | 4 ++-- homeassistant/components/coolmaster/config_flow.py | 2 +- homeassistant/components/kef/media_player.py | 2 +- homeassistant/components/keyboard_remote/__init__.py | 4 ++-- homeassistant/components/mpd/media_player.py | 2 +- homeassistant/components/panasonic_viera/__init__.py | 4 ++-- homeassistant/components/panasonic_viera/config_flow.py | 6 +++--- homeassistant/components/pioneer/media_player.py | 4 ++-- homeassistant/components/rflink/__init__.py | 2 -- homeassistant/components/roomba/config_flow.py | 2 +- homeassistant/components/rtorrent/sensor.py | 2 +- homeassistant/components/webostv/__init__.py | 1 - homeassistant/components/webostv/notify.py | 1 - 13 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index e6cf6f36277..1bcf20f4d5e 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -24,7 +24,7 @@ async def async_setup_entry(hass, entry): info = await coolmaster.info() if not info: raise ConfigEntryNotReady - except (OSError, ConnectionRefusedError, TimeoutError) as error: + except OSError as error: raise ConfigEntryNotReady() from error coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster) hass.data.setdefault(DOMAIN, {}) @@ -64,5 +64,5 @@ class CoolmasterDataUpdateCoordinator(DataUpdateCoordinator): """Fetch data from Coolmaster.""" try: return await self._coolmaster.status() - except (OSError, ConnectionRefusedError, TimeoutError) as error: + except OSError as error: raise UpdateFailed from error diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 1091c24ea31..6a5c517fc85 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -51,7 +51,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result = await _validate_connection(self.hass, host) if not result: errors["base"] = "no_units" - except (OSError, ConnectionRefusedError, TimeoutError): + except OSError: errors["base"] = "cannot_connect" if errors: diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 9452e24a4f2..f32f825acc4 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -256,7 +256,7 @@ class KefMediaPlayer(MediaPlayerEntity): self._source = None self._volume = None self._state = STATE_OFF - except (ConnectionRefusedError, ConnectionError, TimeoutError) as err: + except (ConnectionError, TimeoutError) as err: _LOGGER.debug("Error in `update`: %s", err) self._state = None diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 2ada56e1c44..1d16dd12cc2 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -161,7 +161,7 @@ class KeyboardRemote: # devices are often added and then correct permissions set after try: dev = InputDevice(descriptor) - except (OSError, PermissionError): + except OSError: return (None, None) handler = None @@ -318,7 +318,7 @@ class KeyboardRemote: ): repeat_tasks[event.code].cancel() del repeat_tasks[event.code] - except (OSError, PermissionError, asyncio.CancelledError): + except (OSError, asyncio.CancelledError): # cancel key repeat tasks for task in repeat_tasks.values(): task.cancel() diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index adb4bf0e810..baf57844eaf 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -167,7 +167,7 @@ class MpdDevice(MediaPlayerEntity): self._commands = list(await self._client.commands()) await self._fetch_status() - except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: + except (mpd.ConnectionError, OSError, ValueError) as error: # Cleanly disconnect in case connection is not in valid state _LOGGER.debug("Error updating status: %s", error) self._disconnect() diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 8f0a0e89d45..b217be4d4b6 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -164,7 +164,7 @@ class Remote: if during_setup: await self.async_update() - except (TimeoutError, URLError, SOAPError, OSError) as err: + except (URLError, SOAPError, OSError) as err: _LOGGER.debug("Could not establish remote connection: %s", err) self._control = None self.state = STATE_OFF @@ -251,7 +251,7 @@ class Remote: self.state = STATE_OFF self.available = True await self.async_create_remote_control() - except (TimeoutError, URLError, OSError): + except (URLError, OSError): self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 93c33deb4dc..42400e7348c 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -56,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[ATTR_DEVICE_INFO] = await self.hass.async_add_executor_job( self._remote.get_device_info ) - except (TimeoutError, URLError, SOAPError, OSError) as err: + except (URLError, SOAPError, OSError) as err: _LOGGER.error("Could not establish remote connection: %s", err) errors["base"] = "cannot_connect" except Exception as err: # pylint: disable=broad-except @@ -114,7 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except SOAPError as err: _LOGGER.error("Invalid PIN code: %s", err) errors["base"] = ERROR_INVALID_PIN_CODE - except (TimeoutError, URLError, OSError) as err: + except (URLError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") except Exception as err: # pylint: disable=broad-except @@ -138,7 +138,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.async_add_executor_job( partial(self._remote.request_pin_code, name="Home Assistant") ) - except (TimeoutError, URLError, SOAPError, OSError) as err: + except (URLError, SOAPError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason="cannot_connect") except Exception as err: # pylint: disable=broad-except diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index e573bf0929c..a3e0d318c03 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -112,7 +112,7 @@ class PioneerDevice(MediaPlayerEntity): try: try: telnet = telnetlib.Telnet(self._host, self._port, self._timeout) - except (ConnectionRefusedError, OSError): + except OSError: _LOGGER.warning("Pioneer %s refused connection", self._name) return telnet.write(command.encode("ASCII") + b"\r") @@ -125,7 +125,7 @@ class PioneerDevice(MediaPlayerEntity): """Get the latest details from the device.""" try: telnet = telnetlib.Telnet(self._host, self._port, self._timeout) - except (ConnectionRefusedError, OSError): + except OSError: _LOGGER.warning("Pioneer %s refused connection", self._name) return False diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index c78b0c6f944..9cff8377c35 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -275,8 +275,6 @@ async def async_setup(hass, config): except ( SerialException, - ConnectionRefusedError, - TimeoutError, OSError, asyncio.TimeoutError, ) as exc: diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index c3ccd051dd8..4fdcbceab07 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -206,7 +206,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: password = await self.hass.async_add_executor_job(roomba_pw.get_password) - except (OSError, ConnectionRefusedError): + except OSError: return await self.async_step_link_manual() if not password: diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 4c02f49d86a..c750c7aa83c 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -122,7 +122,7 @@ class RTorrentSensor(SensorEntity): try: self.data = multicall() self._available = True - except (xmlrpc.client.ProtocolError, ConnectionRefusedError, OSError) as ex: + except (xmlrpc.client.ProtocolError, OSError) as ex: _LOGGER.error("Connection to rtorrent failed (%s)", ex) self._available = False return diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 681c2acfe01..af7f59bd266 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -198,7 +198,6 @@ async def async_request_configuration(hass, config, conf, client): except ( OSError, ConnectionClosed, - ConnectionRefusedError, asyncio.TimeoutError, asyncio.CancelledError, PyLGTVCmdException, diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index ece76b5ed32..34277eb3c09 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -55,7 +55,6 @@ class LgWebOSNotificationService(BaseNotificationService): except ( OSError, ConnectionClosed, - ConnectionRefusedError, asyncio.TimeoutError, asyncio.CancelledError, PyLGTVCmdException, From be0d9d185be638390d0781bab4e4dd54e6cf697a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 11 Jun 2021 21:30:58 +1000 Subject: [PATCH 302/750] Bump georss_generic_client to v0.6 (#51745) --- homeassistant/components/geo_rss_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index e7ac2948237..6a470e1ddbd 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -2,7 +2,7 @@ "domain": "geo_rss_events", "name": "GeoRSS", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", - "requirements": ["georss_generic_client==0.4"], + "requirements": ["georss_generic_client==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7bfb16f4111..fc4912afe04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,7 +654,7 @@ geojson_client==0.6 geopy==2.1.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.4 +georss_generic_client==0.6 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 067b35c72ca..66422b82287 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ geojson_client==0.6 geopy==2.1.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.4 +georss_generic_client==0.6 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 From 7d03b02192f3e004673d1deed6c92cdc581fb4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 11 Jun 2021 14:35:03 +0300 Subject: [PATCH 303/750] Spelling fixes (#51642) --- .../components/fireservicerota/switch.py | 2 +- .../components/fritz/binary_sensor.py | 2 +- .../components/fritzbox_callmonitor/base.py | 6 +- .../components/fritzbox_callmonitor/const.py | 2 +- .../components/fritzbox_callmonitor/sensor.py | 4 +- .../google_assistant/report_state.py | 4 +- homeassistant/components/hive/sensor.py | 2 +- .../components/home_plus_control/helpers.py | 2 +- homeassistant/components/homekit/__init__.py | 6 +- .../components/homekit/accessories.py | 6 +- homeassistant/components/homekit/const.py | 2 +- .../hunterdouglas_powerview/cover.py | 4 +- homeassistant/components/isy994/entity.py | 6 +- homeassistant/components/ps4/const.py | 2 +- homeassistant/components/unifi/sensor.py | 4 +- homeassistant/components/zeroconf/models.py | 2 +- tests/components/fritz/test_config_flow.py | 2 +- .../here_travel_time/test_sensor.py | 4 +- tests/components/homekit/test_accessories.py | 4 +- tests/components/homekit/test_aidmanager.py | 2 +- .../homekit/test_get_accessories.py | 4 +- tests/components/hyperion/test_light.py | 2 +- tests/components/influxdb/test_sensor.py | 6 +- tests/components/kraken/test_init.py | 2 +- tests/components/mikrotik/test_hub.py | 2 +- tests/components/sentry/test_config_flow.py | 2 +- tests/components/ssdp/test_init.py | 78 +++++++++---------- .../components/syncthing/test_config_flow.py | 2 +- .../transmission/test_config_flow.py | 2 +- tests/components/vera/common.py | 2 +- tests/components/wilight/__init__.py | 4 +- tests/components/wilight/test_config_flow.py | 8 +- .../xiaomi_aqara/test_config_flow.py | 4 +- tests/components/xiaomi_miio/test_vacuum.py | 12 +-- tests/components/zha/test_device.py | 2 +- tests/helpers/test_condition.py | 4 +- tests/helpers/test_frame.py | 2 +- tests/helpers/test_script.py | 18 ++--- tests/helpers/test_template.py | 2 +- tests/test_config_entries.py | 4 +- 40 files changed, 115 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index f54e3bc1fa2..454048a3737 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -98,7 +98,7 @@ class ResponseSwitch(SwitchEntity): return attr async def async_turn_on(self, **kwargs) -> None: - """Send Acknowlegde response status.""" + """Send Acknowledge response status.""" await self.async_set_response(True) async def async_turn_off(self, **kwargs) -> None: diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 65780fffaa9..bc8fa204ee5 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -1,4 +1,4 @@ -"""AVM FRITZ!Box connectivitiy sensor.""" +"""AVM FRITZ!Box connectivity sensor.""" import logging from fritzconnection.core.exceptions import FritzConnectionException diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py index af0612d7632..0db40e2098f 100644 --- a/homeassistant/components/fritzbox_callmonitor/base.py +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -8,7 +8,7 @@ from fritzconnection.lib.fritzphonebook import FritzPhonebook from homeassistant.util import Throttle -from .const import REGEX_NUMBER, UNKOWN_NAME +from .const import REGEX_NUMBER, UNKNOWN_NAME _LOGGER = logging.getLogger(__name__) @@ -61,13 +61,13 @@ class FritzBoxPhonebook: """Return a name for a given phone number.""" number = re.sub(REGEX_NUMBER, "", str(number)) if self.number_dict is None: - return UNKOWN_NAME + return UNKNOWN_NAME if number in self.number_dict: return self.number_dict[number] if not self.prefixes: - return UNKOWN_NAME + return UNKNOWN_NAME for prefix in self.prefixes: with suppress(KeyError): diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index a71f14401b3..ba0f8d1d973 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -19,7 +19,7 @@ FRITZ_ATTR_NAME = "name" FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" -UNKOWN_NAME = "unknown" +UNKNOWN_NAME = "unknown" SERIAL_NUMBER = "serial_number" REGEX_NUMBER = r"[^\d\+]" diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index a325c0ca71d..63b3cd81aa5 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -42,7 +42,7 @@ from .const import ( STATE_IDLE, STATE_RINGING, STATE_TALKING, - UNKOWN_NAME, + UNKNOWN_NAME, ) _LOGGER = logging.getLogger(__name__) @@ -193,7 +193,7 @@ class FritzBoxCallSensor(SensorEntity): def number_to_name(self, number): """Return a name for a given phone number.""" if self._fritzbox_phonebook is None: - return UNKOWN_NAME + return UNKNOWN_NAME return self._fritzbox_phonebook.get_name(number) def update(self): diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index f7c57732876..c3f8ba3bffd 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -106,7 +106,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig """Check if the serialized data has changed.""" return old_extra_arg != new_extra_arg - async def inital_report(_now): + async def initial_report(_now): """Report initially all states.""" nonlocal unsub, checker entities = {} @@ -140,7 +140,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig MATCH_ALL, async_entity_state_listener ) - unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) + unsub = async_call_later(hass, INITIAL_REPORT_DELAY, initial_report) @callback def unsub_all(): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 518f3286231..f21afc51801 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,4 +1,4 @@ -"""Support for the Hive sesnors.""" +"""Support for the Hive sensors.""" from datetime import timedelta diff --git a/homeassistant/components/home_plus_control/helpers.py b/homeassistant/components/home_plus_control/helpers.py index 773732b1a50..f5687a23c66 100644 --- a/homeassistant/components/home_plus_control/helpers.py +++ b/homeassistant/components/home_plus_control/helpers.py @@ -20,7 +20,7 @@ class HomePlusControlOAuth2Implementation( subscription_key (str): Subscription key obtained from the API provider. authorize_url (str): Authorization URL initiate authentication flow. token_url (str): URL to retrieve access/refresh tokens. - name (str): Name of the implementation (appears in the HomeAssitant GUI). + name (str): Name of the implementation (appears in the HomeAssistant GUI). """ def __init__( diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 3f297892446..46fd3e5e522 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -59,7 +59,7 @@ from . import ( # noqa: F401 from .accessories import HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( - ATTR_INTERGRATION, + ATTR_INTEGRATION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, @@ -767,9 +767,9 @@ class HomeKit: integration = await async_get_integration( self.hass, ent_reg_ent.platform ) - ent_cfg[ATTR_INTERGRATION] = integration.name + ent_cfg[ATTR_INTEGRATION] = integration.name except IntegrationNotFound: - ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform + ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform class HomeKitPairingQRView(HomeAssistantView): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3aeaa31faed..b9bd62246cf 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -42,7 +42,7 @@ from homeassistant.util.decorator import Registry from .const import ( ATTR_DISPLAY_NAME, - ATTR_INTERGRATION, + ATTR_INTEGRATION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, @@ -221,8 +221,8 @@ class HomeAccessory(Accessory): if ATTR_MANUFACTURER in self.config: manufacturer = self.config[ATTR_MANUFACTURER] - elif ATTR_INTERGRATION in self.config: - manufacturer = self.config[ATTR_INTERGRATION].replace("_", " ").title() + elif ATTR_INTEGRATION in self.config: + manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title() else: manufacturer = f"{MANUFACTURER} {domain}".title() if ATTR_MODEL in self.config: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 073650aba40..37788f9dca7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -21,7 +21,7 @@ AUDIO_CODEC_COPY = "copy" # #### Attributes #### ATTR_DISPLAY_NAME = "display_name" ATTR_VALUE = "value" -ATTR_INTERGRATION = "platform" +ATTR_INTEGRATION = "platform" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" ATTR_SOFTWARE_VERSION = "sw_version" diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 58c7e90994c..901a048fc7f 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -91,9 +91,9 @@ def hd_position_to_hass(hd_position): return round((hd_position / MAX_POSITION) * 100) -def hass_position_to_hd(hass_positon): +def hass_position_to_hd(hass_position): """Convert hass position to hunter douglas position.""" - return int(hass_positon / 100 * MAX_POSITION) + return int(hass_position / 100 * MAX_POSITION) class PowerViewShade(ShadeEntity, CoverEntity): diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 69714dd9f4b..0406fc45cba 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -180,7 +180,7 @@ class ISYNodeEntity(ISYEntity): await self._node.send_cmd(command, value, unit_of_measurement, parameters) async def async_get_zwave_parameter(self, parameter): - """Repsond to an entity service command to request a Z-Wave device parameter from the ISY.""" + """Respond to an entity service command to request a Z-Wave device parameter from the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( f"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave device {self.entity_id}" @@ -188,7 +188,7 @@ class ISYNodeEntity(ISYEntity): await self._node.get_zwave_parameter(parameter) async def async_set_zwave_parameter(self, parameter, value, size): - """Repsond to an entity service command to set a Z-Wave device parameter via the ISY.""" + """Respond to an entity service command to set a Z-Wave device parameter via the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( f"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave device {self.entity_id}" @@ -197,7 +197,7 @@ class ISYNodeEntity(ISYEntity): await self._node.get_zwave_parameter(parameter) async def async_rename_node(self, name): - """Repsond to an entity service command to rename a node on the ISY.""" + """Respond to an entity service command to rename a node on the ISY.""" await self._node.rename(name) diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index f2d284daa79..c5236f7dffe 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -54,7 +54,7 @@ COUNTRYCODE_NAMES = { "LU": "Luxembourg", "MT": "Malta", "MX": "Mexico", - "MY": "Maylasia", + "MY": "Maylasia", # spelling error compatibility with pyps4_2ndscreen.media_art.COUNTRIES "NI": "Nicaragua", "NL": "Nederland", "NO": "Norway", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 14456bb8c06..8d34d3cabd7 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -> None: """Update the values of the controller.""" if controller.option_allow_bandwidth_sensors: - add_bandwith_entities(controller, async_add_entities, clients) + add_bandwidth_entities(controller, async_add_entities, clients) if controller.option_allow_uptime_sensors: add_uptime_entities(controller, async_add_entities, clients) @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def add_bandwith_entities(controller, async_add_entities, clients): +def add_bandwidth_entities(controller, async_add_entities, clients): """Add new sensor entities from the controller.""" sensors = [] diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py index c09e6428f2a..5a59ba52e3f 100644 --- a/homeassistant/components/zeroconf/models.py +++ b/homeassistant/components/zeroconf/models.py @@ -43,7 +43,7 @@ class HaServiceBrowser(ServiceBrowser): # As the list of zeroconf names we watch for grows, each additional # ServiceBrowser would process all the A and AAAA updates on the network. # - # To avoid overwhemling the system we pre-filter here and only process + # To avoid overwhelming the system we pre-filter here and only process # DNSPointers for the configured record name (type) # if record.name not in self.types or not isinstance(record, DNSPointer): diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6e051ef1bdd..a68aee2edff 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -305,7 +305,7 @@ async def test_ssdp_already_configured_host(hass: HomeAssistant, fc_class_mock): async def test_ssdp_already_configured_host_uuid(hass: HomeAssistant, fc_class_mock): - """Test starting a flow from discovery with a laready configured uuid.""" + """Test starting a flow from discovery with an already configured uuid.""" mock_config = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 2f69dc97a84..3e5b2aeaaed 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -132,7 +132,7 @@ def requests_mock_credentials_check(requests_mock): @pytest.fixture def requests_mock_truck_response(requests_mock_credentials_check): - """Return a requests_mock for truck respones.""" + """Return a requests_mock for truck response.""" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED] response_url = _build_mock_url( ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), @@ -147,7 +147,7 @@ def requests_mock_truck_response(requests_mock_credentials_check): @pytest.fixture def requests_mock_car_disabled_response(requests_mock_credentials_check): - """Return a requests_mock for truck respones.""" + """Return a requests_mock for truck response.""" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] response_url = _build_mock_url( ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index afaa9ea0892..84ed61322a2 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.accessories import ( ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, - ATTR_INTERGRATION, + ATTR_INTEGRATION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, @@ -106,7 +106,7 @@ async def test_home_accessory(hass, hk_driver): ATTR_MODEL: "Awesome", ATTR_MANUFACTURER: "Lux Brands", ATTR_SOFTWARE_VERSION: "0.4.3", - ATTR_INTERGRATION: "luxe", + ATTR_INTEGRATION: "luxe", }, ) assert acc3.available is False diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index df1bb14dd9e..dd9daaac43d 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -77,7 +77,7 @@ async def test_aid_generation(hass, device_reg, entity_reg): aid_storage.delete_aid(get_system_unique_id(light_ent)) aid_storage.delete_aid(get_system_unique_id(light_ent2)) aid_storage.delete_aid(get_system_unique_id(remote_ent)) - aid_storage.delete_aid("non-existant-one") + aid_storage.delete_aid("non-existent-one") for _ in range(0, 2): assert ( diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 1c68ae7d001..491d686162d 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -7,7 +7,7 @@ import homeassistant.components.climate as climate import homeassistant.components.cover as cover from homeassistant.components.homekit.accessories import TYPES, get_accessory from homeassistant.components.homekit.const import ( - ATTR_INTERGRATION, + ATTR_INTEGRATION, CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, @@ -66,7 +66,7 @@ def test_customize_options(config, name): """Test with customized options.""" mock_type = Mock() conf = config.copy() - conf[ATTR_INTERGRATION] = "platform_name" + conf[ATTR_INTEGRATION] = "platform_name" with patch.dict(TYPES, {"Light": mock_type}): entity_state = State("light.demo", "on") get_accessory(None, None, entity_state, 2, conf) diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 866b1b1b1a8..829a76f22d3 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -241,7 +241,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistant) -> None assert hass.states.get(TEST_ENTITY_ID_3) is not None -async def test_light_basic_properies(hass: HomeAssistant) -> None: +async def test_light_basic_properties(hass: HomeAssistant) -> None: """Test the basic properties.""" client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 9a353f59e42..1df106473f9 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -313,7 +313,7 @@ async def test_config_failure(hass, config_ext): async def test_state_matches_query_result( hass, mock_client, config_ext, queries, set_query_mock, make_resultset ): - """Test state of sensor matches respone from query api.""" + """Test state of sensor matches response from query api.""" set_query_mock(mock_client, return_value=make_resultset(42)) sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) @@ -344,7 +344,7 @@ async def test_state_matches_query_result( async def test_state_matches_first_query_result_for_multiple_return( hass, caplog, mock_client, config_ext, queries, set_query_mock, make_resultset ): - """Test state of sensor matches respone from query api.""" + """Test state of sensor matches response from query api.""" set_query_mock(mock_client, return_value=make_resultset(42, "not used")) sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) @@ -370,7 +370,7 @@ async def test_state_matches_first_query_result_for_multiple_return( async def test_state_for_no_results( hass, caplog, mock_client, config_ext, queries, set_query_mock ): - """Test state of sensor matches respone from query api.""" + """Test state of sensor matches response from query api.""" set_query_mock(mock_client) sensors = await _setup(hass, config_ext, queries, ["sensor.test"]) diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py index 742e48eb1c0..99af317154d 100644 --- a/tests/components/kraken/test_init.py +++ b/tests/components/kraken/test_init.py @@ -28,7 +28,7 @@ async def test_unload_entry(hass): assert DOMAIN not in hass.data -async def test_unkown_error(hass, caplog): +async def test_unknown_error(hass, caplog): """Test unload for Kraken.""" with patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 859c7d20d04..2159b58293b 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def setup_mikrotik_entry(hass, **kwargs): - """Set up Mikrotik intergation successfully.""" + """Set up Mikrotik integration successfully.""" support_wireless = kwargs.get("support_wireless", True) dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 2246cabe33a..2876f6e3a17 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -87,7 +87,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: assert result2.get("errors") == {"base": "bad_dsn"} -async def test_user_flow_unkown_exception(hass: HomeAssistant) -> None: +async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: """Test we handle any unknown exception error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 2b527064ff8..6c019f1f311 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -119,7 +119,7 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): "location": "http://1.1.1.1", } mock_init = await _async_run_mocked_scan(hass, mock_ssdp_response, mock_get_ssdp) - # If we get duplicate respones, ensure we only look it up once + # If we get duplicate response, ensure we only look it up once assert len(aioclient_mock.mock_calls) == 1 assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -363,11 +363,11 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): "x-rincon-bootseq": "55", "ext": "", } - not_matching_intergration_callbacks = [] - intergration_match_all_callbacks = [] - intergration_match_all_not_present_callbacks = [] - intergration_callbacks = [] - intergration_callbacks_from_cache = [] + not_matching_integration_callbacks = [] + integration_match_all_callbacks = [] + integration_match_all_not_present_callbacks = [] + integration_callbacks = [] + integration_callbacks_from_cache = [] match_any_callbacks = [] @callback @@ -375,24 +375,24 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): raise ValueError @callback - def _async_intergration_callbacks(info): - intergration_callbacks.append(info) + def _async_integration_callbacks(info): + integration_callbacks.append(info) @callback - def _async_intergration_match_all_callbacks(info): - intergration_match_all_callbacks.append(info) + def _async_integration_match_all_callbacks(info): + integration_match_all_callbacks.append(info) @callback - def _async_intergration_match_all_not_present_callbacks(info): - intergration_match_all_not_present_callbacks.append(info) + def _async_integration_match_all_not_present_callbacks(info): + integration_match_all_not_present_callbacks.append(info) @callback - def _async_intergration_callbacks_from_cache(info): - intergration_callbacks_from_cache.append(info) + def _async_integration_callbacks_from_cache(info): + integration_callbacks_from_cache.append(info) @callback - def _async_not_matching_intergration_callbacks(info): - not_matching_intergration_callbacks.append(info) + def _async_not_matching_integration_callbacks(info): + not_matching_integration_callbacks.append(info) @callback def _async_match_any_callbacks(info): @@ -422,22 +422,22 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): ssdp.async_register_callback(hass, _async_exception_callbacks, {}) ssdp.async_register_callback( hass, - _async_intergration_callbacks, + _async_integration_callbacks, {"st": "mock-st"}, ) ssdp.async_register_callback( hass, - _async_intergration_match_all_callbacks, + _async_integration_match_all_callbacks, {"x-rincon-bootseq": MATCH_ALL}, ) ssdp.async_register_callback( hass, - _async_intergration_match_all_not_present_callbacks, + _async_integration_match_all_not_present_callbacks, {"x-not-there": MATCH_ALL}, ) ssdp.async_register_callback( hass, - _async_not_matching_intergration_callbacks, + _async_not_matching_integration_callbacks, {"st": "not-match-mock-st"}, ) ssdp.async_register_callback( @@ -448,7 +448,7 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) ssdp.async_register_callback( hass, - _async_intergration_callbacks_from_cache, + _async_integration_callbacks_from_cache, {"st": "mock-st"}, ) await hass.async_block_till_done() @@ -459,13 +459,13 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): await hass.async_block_till_done() assert hass.state == CoreState.running - assert len(intergration_callbacks) == 3 - assert len(intergration_callbacks_from_cache) == 3 - assert len(intergration_match_all_callbacks) == 3 - assert len(intergration_match_all_not_present_callbacks) == 0 + assert len(integration_callbacks) == 3 + assert len(integration_callbacks_from_cache) == 3 + assert len(integration_match_all_callbacks) == 3 + assert len(integration_match_all_not_present_callbacks) == 0 assert len(match_any_callbacks) == 3 - assert len(not_matching_intergration_callbacks) == 0 - assert intergration_callbacks[0] == { + assert len(not_matching_integration_callbacks) == 0 + assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_EXT: "", ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", @@ -503,11 +503,11 @@ async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog "x-rincon-variant": "1", "household.smartspeaker.audio": "Sonos_v3294823948542543534", } - intergration_callbacks = [] + integration_callbacks = [] @callback - def _async_intergration_callbacks(info): - intergration_callbacks.append(info) + def _async_integration_callbacks(info): + integration_callbacks.append(info) def _generate_fake_ssdp_listener(*args, **kwargs): listener = SSDPListener(*args, **kwargs) @@ -532,7 +532,7 @@ async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog await hass.async_block_till_done() ssdp.async_register_callback( hass, - _async_intergration_callbacks, + _async_integration_callbacks, {"nts": "ssdp:alive", "x-rincon-bootseq": MATCH_ALL}, ) await hass.async_block_till_done() @@ -546,9 +546,9 @@ async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog assert hass.state == CoreState.running assert ( - len(intergration_callbacks) == 2 + len(integration_callbacks) == 2 ) # unsolicited callbacks without st are not cached - assert intergration_callbacks[0] == { + assert integration_callbacks[0] == { "UDN": "uuid:RINCON_1111BB963FD801400", "bootid.upnp.org": "250", "deviceType": "Paulus", @@ -589,11 +589,11 @@ async def test_scan_second_hit(hass, aioclient_mock, caplog): } ) mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]} - intergration_callbacks = [] + integration_callbacks = [] @callback - def _async_intergration_callbacks(info): - intergration_callbacks.append(info) + def _async_integration_callbacks(info): + integration_callbacks.append(info) def _generate_fake_ssdp_listener(*args, **kwargs): listener = SSDPListener(*args, **kwargs) @@ -622,7 +622,7 @@ async def test_scan_second_hit(hass, aioclient_mock, caplog): await hass.async_block_till_done() remove = ssdp.async_register_callback( hass, - _async_intergration_callbacks, + _async_integration_callbacks, {"st": "mock-st"}, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -635,8 +635,8 @@ async def test_scan_second_hit(hass, aioclient_mock, caplog): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - assert len(intergration_callbacks) == 2 - assert intergration_callbacks[0] == { + assert len(integration_callbacks) == 2 + assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_EXT: "", ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 30f8bc0386b..7cdf728c07f 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -34,7 +34,7 @@ async def test_show_setup_form(hass): assert result["step_id"] == "user" -async def test_flow_successfull(hass): +async def test_flow_successful(hass): """Test with required fields only.""" with patch( "aiosyncthing.system.System.status", return_value={"myID": "server-id"} diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 79b341e4504..91dfa25fd35 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -296,7 +296,7 @@ async def test_error_on_connection_failure(hass, conn_error): assert result["errors"] == {"base": "cannot_connect"} -async def test_error_on_unknwon_error(hass, unknown_error): +async def test_error_on_unknown_error(hass, unknown_error): """Test when connection to host fails.""" flow = init_config_flow(hass) diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index ae3c0a1a1de..1ce55ac9e8f 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -65,7 +65,7 @@ def new_simple_controller_config( setup_callback: SetupCallback = None, legacy_entity_unique_id=False, ) -> ControllerConfig: - """Create simple contorller config.""" + """Create simple controller config.""" return ControllerConfig( config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, options=options, diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index d16b4d083e8..dd7d83876f8 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -41,7 +41,7 @@ MOCK_SSDP_DISCOVERY_INFO_P_B = { ATTR_UPNP_SERIAL: UPNP_SERIAL, } -MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = { +MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = { ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT, ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, @@ -49,7 +49,7 @@ MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = { ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, } -MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = { +MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = { ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 42f6aa592b0..4835167715d 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -21,9 +21,9 @@ from tests.common import MockConfigEntry from tests.components.wilight import ( CONF_COMPONENTS, HOST, - MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER, + MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER, MOCK_SSDP_DISCOVERY_INFO_P_B, - MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER, + MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER, UPNP_MODEL_NAME_P_B, UPNP_SERIAL, WILIGHT_ID, @@ -71,7 +71,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER.copy() + discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) @@ -83,7 +83,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: """Test that the ssdp aborts not_wilight.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER.copy() + discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 859338b82d3..3f445a1fdec 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -113,7 +113,7 @@ async def test_config_flow_user_success(hass): async def test_config_flow_user_multiple_success(hass): - """Test a successful config flow initialized by the user with multiple gateways discoverd.""" + """Test a successful config flow initialized by the user with multiple gateways discovered.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -249,7 +249,7 @@ async def test_config_flow_user_host_mac_success(hass): async def test_config_flow_user_discovery_error(hass): - """Test a failed config flow initialized by the user with no gateways discoverd.""" + """Test a failed config flow initialized by the user with no gateways discovered.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index fe0466472fa..0eb806c0a64 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -115,8 +115,8 @@ def mirobo_is_got_error_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls: - mock_vaccum_cls.return_value = mock_vacuum + with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -143,8 +143,8 @@ def mirobo_old_speeds_fixture(request): mock_vacuum.fan_speed_presets.return_value = request.param mock_vacuum.status().fanspeed = list(request.param.values())[0] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls: - mock_vaccum_cls.return_value = mock_vacuum + with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -189,8 +189,8 @@ def mirobo_is_on_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vaccum_cls: - mock_vaccum_cls.return_value = mock_vacuum + with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index d5ed0152b8b..0f696f21572 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -192,7 +192,7 @@ async def test_check_available_unsuccessful( assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] assert zha_device.available is True - # not even trying to update, device is unavailble + # not even trying to update, device is unavailable _send_time_changed(hass, 91) await hass.async_block_till_done() assert basic_ch.read_attributes.await_count == 2 diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index d75dd53bbf2..bd5e15ad11f 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1034,7 +1034,7 @@ async def test_state_attribute(hass): }, ) - hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 200}) + hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 200}) with pytest.raises(ConditionError): test(hass) @@ -1330,7 +1330,7 @@ async def test_numeric_state_attribute(hass): }, ) - hass.states.async_set("sensor.temperature", 100, {"unkown_attr": 10}) + hass.states.async_set("sensor.temperature", 100, {"unknown_attr": 10}) with pytest.raises(ConditionError): assert test(hass) diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index b198a16adb1..5e48b2aec5f 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -15,7 +15,7 @@ async def test_extract_frame_integration(caplog, mock_integration_frame): assert found_frame == mock_integration_frame -async def test_extract_frame_integration_with_excluded_intergration(caplog): +async def test_extract_frame_integration_with_excluded_integration(caplog): """Test extracting the current frame from integration context.""" correct_frame = Mock( filename="/home/dev/homeassistant/components/mdns/light.py", diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 546f494735e..0af8ff7d431 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -477,7 +477,7 @@ async def test_stop_no_wait(hass, count): # Can't assert just yet because we haven't verified stopping works yet. # If assert fails we can hang test if async_stop doesn't work. - script_was_runing = script_obj.is_running + script_was_running = script_obj.is_running were_no_events = len(events) == 0 # Begin the process of stopping the script (which should stop all runs), and then @@ -487,7 +487,7 @@ async def test_stop_no_wait(hass, count): await hass.async_block_till_done() - assert script_was_runing + assert script_was_running assert were_no_events assert not script_obj.is_running assert len(events) == 0 @@ -1189,8 +1189,8 @@ async def test_wait_template_with_utcnow(hass): start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) try: - non_maching_time = start_time.replace(hour=3) - with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): + non_matching_time = start_time.replace(hour=3) + with patch("homeassistant.util.dt.utcnow", return_value=non_matching_time): hass.async_create_task(script_obj.async_run(context=Context())) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running @@ -1221,17 +1221,17 @@ async def test_wait_template_with_utcnow_no_match(hass): timed_out = False try: - non_maching_time = start_time.replace(hour=3) - with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): + non_matching_time = start_time.replace(hour=3) + with patch("homeassistant.util.dt.utcnow", return_value=non_matching_time): hass.async_create_task(script_obj.async_run(context=Context())) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running - second_non_maching_time = start_time.replace(hour=4) + second_non_matching_time = start_time.replace(hour=4) with patch( - "homeassistant.util.dt.utcnow", return_value=second_non_maching_time + "homeassistant.util.dt.utcnow", return_value=second_non_matching_time ): - async_fire_time_changed(hass, second_non_maching_time) + async_fire_time_changed(hass, second_non_matching_time) with timeout(0.1): await hass.async_block_till_done() diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 1818d8c4876..2547537bff9 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2499,7 +2499,7 @@ async def test_no_result_parsing(hass): async def test_is_static_still_ast_evals(hass): - """Test is_static still convers to native type.""" + """Test is_static still converts to native type.""" tpl = template.Template("[1, 2]", hass) assert tpl.is_static assert tpl.async_render() == [1, 2] diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1fca4b061cc..615b97fb990 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1971,7 +1971,7 @@ async def test__async_current_entries_does_not_skip_ignore_non_user(hass, manage assert len(mock_setup_entry.mock_calls) == 0 -async def test__async_current_entries_explict_skip_ignore(hass, manager): +async def test__async_current_entries_explicit_skip_ignore(hass, manager): """Test that _async_current_entries can explicitly include ignore.""" hass.config.components.add("comp") entry = MockConfigEntry( @@ -2010,7 +2010,7 @@ async def test__async_current_entries_explict_skip_ignore(hass, manager): assert p_entry.data == {"token": "supersecret"} -async def test__async_current_entries_explict_include_ignore(hass, manager): +async def test__async_current_entries_explicit_include_ignore(hass, manager): """Test that _async_current_entries can explicitly include ignore.""" hass.config.components.add("comp") entry = MockConfigEntry( From 343e0e0933b3ebf75e4c14ec33594ef863b0963b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 11 Jun 2021 13:36:17 +0200 Subject: [PATCH 304/750] Use attrs instead of properties in Brother (#51742) * Use attrs instead of properties * Use get() for device_class --- homeassistant/components/brother/const.py | 8 +++- homeassistant/components/brother/model.py | 3 +- homeassistant/components/brother/sensor.py | 55 +++++----------------- 3 files changed, 20 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 9e2c096ac76..c0021df11fc 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -4,7 +4,12 @@ from __future__ import annotations from typing import Final from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT -from homeassistant.const import ATTR_ICON, PERCENTAGE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, +) from .model import SensorDescription @@ -247,5 +252,6 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT: None, ATTR_ENABLED: False, ATTR_STATE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, } diff --git a/homeassistant/components/brother/model.py b/homeassistant/components/brother/model.py index d53327ae22a..ab8df09b749 100644 --- a/homeassistant/components/brother/model.py +++ b/homeassistant/components/brother/model.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TypedDict -class SensorDescription(TypedDict): +class SensorDescription(TypedDict, total=False): """Sensor description class.""" icon: str | None @@ -12,3 +12,4 @@ class SensorDescription(TypedDict): unit: str | None enabled: bool state_class: str | None + device_class: str | None diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 773da1d09b1..38fac529076 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ICON, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -60,18 +60,17 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._description = SENSOR_TYPES[kind] - self._name = f"{coordinator.data.model} {self._description[ATTR_LABEL]}" - self._unique_id = f"{coordinator.data.serial.lower()}_{kind}" - self._device_info = device_info - self.kind = kind + description = SENSOR_TYPES[kind] self._attrs: dict[str, Any] = {} - self._attr_state_class = self._description[ATTR_STATE_CLASS] - - @property - def name(self) -> str: - """Return the name.""" - return self._name + self._attr_device_class = description.get(ATTR_DEVICE_CLASS) + self._attr_device_info = device_info + self._attr_entity_registry_enabled_default = description[ATTR_ENABLED] + self._attr_icon = description[ATTR_ICON] + self._attr_name = f"{coordinator.data.model} {description[ATTR_LABEL]}" + self._attr_state_class = description[ATTR_STATE_CLASS] + self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}" + self._attr_unit_of_measurement = description[ATTR_UNIT] + self.kind = kind @property def state(self) -> Any: @@ -80,13 +79,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): return getattr(self.coordinator.data, self.kind).isoformat() return getattr(self.coordinator.data, self.kind) - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - if self.kind == ATTR_UPTIME: - return DEVICE_CLASS_TIMESTAMP - return None - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" @@ -97,28 +89,3 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): ) self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter) return self._attrs - - @property - def icon(self) -> str | None: - """Return the icon.""" - return self._description[ATTR_ICON] - - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - return self._description[ATTR_UNIT] - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return self._device_info - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._description[ATTR_ENABLED] From e0013648f624df8e73234d02a1b5fa909434d64c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 11 Jun 2021 06:48:20 -0500 Subject: [PATCH 305/750] Use attrs instead of properties in sonarr (#51737) * Use attrs instead of properties in sonarr * Create entity.py * Update sensor.py * Update __init__.py * Update entity.py * Update entity.py * Update sensor.py --- homeassistant/components/sonarr/__init__.py | 56 --------------------- homeassistant/components/sonarr/entity.py | 39 ++++++++++++++ homeassistant/components/sonarr/sensor.py | 24 +++------ 3 files changed, 46 insertions(+), 73 deletions(-) create mode 100644 homeassistant/components/sonarr/entity.py diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 730ba857c49..08fc2c7f26d 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -8,7 +8,6 @@ from sonarr import Sonarr, SonarrAccessRestricted, SonarrError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_NAME, CONF_API_KEY, CONF_HOST, CONF_PORT, @@ -18,12 +17,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_SOFTWARE_VERSION, CONF_BASE_PATH, CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, @@ -99,54 +94,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) - - -class SonarrEntity(Entity): - """Defines a base Sonarr entity.""" - - def __init__( - self, - *, - sonarr: Sonarr, - entry_id: str, - device_id: str, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the Sonarr entity.""" - self._entry_id = entry_id - self._device_id = device_id - self._enabled_default = enabled_default - self._icon = icon - self._name = name - self.sonarr = sonarr - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def device_info(self) -> DeviceInfo | None: - """Return device information about the application.""" - if self._device_id is None: - return None - - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: "Activity Sensor", - ATTR_MANUFACTURER: "Sonarr", - ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version, - "entry_type": "service", - } diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py new file mode 100644 index 00000000000..3fc74b1ddb5 --- /dev/null +++ b/homeassistant/components/sonarr/entity.py @@ -0,0 +1,39 @@ +"""Base Entity for Sonarr.""" +from __future__ import annotations + +from sonarr import Sonarr + +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_SOFTWARE_VERSION, DOMAIN + + +class SonarrEntity(Entity): + """Defines a base Sonarr entity.""" + + def __init__( + self, + *, + sonarr: Sonarr, + entry_id: str, + device_id: str, + ) -> None: + """Initialize the Sonarr entity.""" + self._entry_id = entry_id + self._device_id = device_id + self.sonarr = sonarr + + @property + def device_info(self) -> DeviceInfo | None: + """Return device information about the application.""" + if self._device_id is None: + return None + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: "Activity Sensor", + ATTR_MANUFACTURER: "Sonarr", + ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version, + "entry_type": "service", + } diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 392c026f49b..f1413aca52f 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -14,8 +14,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from . import SonarrEntity from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DATA_SONARR, DOMAIN +from .entity import SonarrEntity _LOGGER = logging.getLogger(__name__) @@ -81,35 +81,25 @@ class SonarrSensor(SonarrEntity, SensorEntity): unit_of_measurement: str | None = None, ) -> None: """Initialize Sonarr sensor.""" - self._unit_of_measurement = unit_of_measurement self._key = key - self._unique_id = f"{entry_id}_{key}" + self._attr_name = name + self._attr_icon = icon + self._attr_unique_id = f"{entry_id}_{key}" + self._attr_unit_of_measurement = unit_of_measurement + self._attr_entity_registry_enabled_default = enabled_default self.last_update_success = False super().__init__( sonarr=sonarr, entry_id=entry_id, device_id=entry_id, - name=name, - icon=icon, - enabled_default=enabled_default, ) - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return self._unique_id - @property def available(self) -> bool: """Return sensor availability.""" return self.last_update_success - @property - def unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - class SonarrCommandsSensor(SonarrSensor): """Defines a Sonarr Commands sensor.""" @@ -186,7 +176,7 @@ class SonarrDiskspaceSensor(SonarrSensor): attrs[ disk.path - ] = f"{free:.2f}/{total:.2f}{self._unit_of_measurement} ({usage:.2f}%)" + ] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)" return attrs From fa3ae9b83c448e0971a470d8c81e94d956310fa8 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 11 Jun 2021 06:51:18 -0500 Subject: [PATCH 306/750] Use attrs instead of properties in roku (#51735) * Use attrs instead of properties in roku. * Update media_player.py * Update remote.py * Update __init__.py * Create entity.py * Update entity.py * Update media_player.py * Update remote.py * Update __init__.py * Update media_player.py * Update remote.py * Update __init__.py * Update __init__.py * Update entity.py --- homeassistant/components/roku/__init__.py | 50 ++----------------- homeassistant/components/roku/entity.py | 42 ++++++++++++++++ homeassistant/components/roku/media_player.py | 12 ++--- homeassistant/components/roku/remote.py | 12 ++--- 4 files changed, 53 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/roku/entity.py diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index e81f5260ac1..bc85915f39a 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -10,26 +10,14 @@ from rokuecp.models import Device from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utcnow -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - ATTR_SUGGESTED_AREA, - DOMAIN, -) +from .const import DOMAIN CONFIG_SCHEMA = cv.deprecated(DOMAIN) @@ -114,35 +102,3 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): return data except RokuError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error - - -class RokuEntity(CoordinatorEntity): - """Defines a base Roku entity.""" - - def __init__( - self, *, device_id: str, name: str, coordinator: RokuDataUpdateCoordinator - ) -> None: - """Initialize the Roku entity.""" - super().__init__(coordinator) - self._device_id = device_id - self._name = name - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this Roku device.""" - if self._device_id is None: - return None - - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.name, - ATTR_MANUFACTURER: self.coordinator.data.info.brand, - ATTR_MODEL: self.coordinator.data.info.model_name, - ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, - ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location, - } diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py new file mode 100644 index 00000000000..aefc335e64d --- /dev/null +++ b/homeassistant/components/roku/entity.py @@ -0,0 +1,42 @@ +"""Base Entity for Roku.""" +from __future__ import annotations + +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import RokuDataUpdateCoordinator +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + ATTR_SUGGESTED_AREA, + DOMAIN, +) + + +class RokuEntity(CoordinatorEntity): + """Defines a base Roku entity.""" + + def __init__( + self, *, device_id: str, coordinator: RokuDataUpdateCoordinator + ) -> None: + """Initialize the Roku entity.""" + super().__init__(coordinator) + self._device_id = device_id + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Roku device.""" + if self._device_id is None: + return None + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: self.name, + ATTR_MANUFACTURER: self.coordinator.data.info.brand, + ATTR_MODEL: self.coordinator.data.info.model_name, + ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + ATTR_SUGGESTED_AREA: self.coordinator.data.info.device_location, + } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index ce5a77f06f6..dc0f2ff704c 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -37,9 +37,10 @@ from homeassistant.const import ( from homeassistant.helpers import entity_platform from homeassistant.helpers.network import is_internal_request -from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler +from . import RokuDataUpdateCoordinator, roku_exception_handler from .browse_media import build_item_response, library_payload from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH +from .entity import RokuEntity _LOGGER = logging.getLogger(__name__) @@ -82,11 +83,11 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Initialize the Roku device.""" super().__init__( coordinator=coordinator, - name=coordinator.data.info.name, device_id=unique_id, ) - self._unique_id = unique_id + self._attr_name = coordinator.data.info.name + self._attr_unique_id = unique_id def _media_playback_trackable(self) -> bool: """Detect if we have enough media data to track playback.""" @@ -95,11 +96,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return self.coordinator.data.media.duration > 0 - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self._unique_id - @property def device_class(self) -> str | None: """Return the class of this device.""" diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 7eb8396d6fa..28095311d81 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -6,8 +6,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler +from . import RokuDataUpdateCoordinator, roku_exception_handler from .const import DOMAIN +from .entity import RokuEntity async def async_setup_entry( @@ -28,16 +29,11 @@ class RokuRemote(RokuEntity, RemoteEntity): """Initialize the Roku device.""" super().__init__( device_id=unique_id, - name=coordinator.data.info.name, coordinator=coordinator, ) - self._unique_id = unique_id - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self._unique_id + self._attr_name = coordinator.data.info.name + self._attr_unique_id = unique_id @property def is_on(self) -> bool: From b01b33c30409eea2b3f19f18348a33879df1c369 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Jun 2021 15:05:57 +0200 Subject: [PATCH 307/750] Add trigger condition (#51710) * Add trigger condition * Tweaks, add tests --- homeassistant/helpers/condition.py | 21 +++++ homeassistant/helpers/config_validation.py | 22 +++-- homeassistant/helpers/trigger.py | 5 +- tests/components/automation/test_init.py | 94 ++++++++++++++++++++++ tests/helpers/test_condition.py | 14 ++++ 5 files changed, 149 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d3020b8d6d8..cea79c4fc8f 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -31,6 +31,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_ID, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WEEKDAY, @@ -930,6 +931,26 @@ async def async_device_from_config( ) +async def async_trigger_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Test a trigger condition.""" + if config_validation: + config = cv.TRIGGER_CONDITION_SCHEMA(config) + trigger_id = config[CONF_ID] + + @trace_condition_function + def trigger_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + """Validate trigger based if-condition.""" + return ( + variables is not None + and "trigger" in variables + and variables["trigger"].get("id") in trigger_id + ) + + return trigger_if + + async def async_validate_condition_config( hass: HomeAssistant, config: ConfigType | Template ) -> ConfigType | Template: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index feb03cf04a2..e195c1ded31 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -45,6 +45,7 @@ from homeassistant.const import ( CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE, CONF_FOR, + CONF_ID, CONF_PLATFORM, CONF_REPEAT, CONF_SCAN_INTERVAL, @@ -1026,6 +1027,14 @@ TIME_CONDITION_SCHEMA = vol.All( has_at_least_one_key("before", "after", "weekday"), ) +TRIGGER_CONDITION_SCHEMA = vol.Schema( + { + **CONDITION_BASE_SCHEMA, + vol.Required(CONF_CONDITION): "trigger", + vol.Required(CONF_ID): vol.All(ensure_list, [string]), + } +) + ZONE_CONDITION_SCHEMA = vol.Schema( { **CONDITION_BASE_SCHEMA, @@ -1090,23 +1099,26 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema( key_value_schemas( CONF_CONDITION, { + "and": AND_CONDITION_SCHEMA, + "device": DEVICE_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, + "or": OR_CONDITION_SCHEMA, "state": STATE_CONDITION_SCHEMA, "sun": SUN_CONDITION_SCHEMA, "template": TEMPLATE_CONDITION_SCHEMA, "time": TIME_CONDITION_SCHEMA, + "trigger": TRIGGER_CONDITION_SCHEMA, "zone": ZONE_CONDITION_SCHEMA, - "and": AND_CONDITION_SCHEMA, - "or": OR_CONDITION_SCHEMA, - "not": NOT_CONDITION_SCHEMA, - "device": DEVICE_CONDITION_SCHEMA, }, ), dynamic_template, ) ) -TRIGGER_BASE_SCHEMA = vol.Schema({vol.Required(CONF_PLATFORM): str}) +TRIGGER_BASE_SCHEMA = vol.Schema( + {vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str} +) TRIGGER_SCHEMA = vol.All( ensure_list, [TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)] diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 045c56d964c..64c5373d8f5 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -8,7 +8,7 @@ from typing import Any, Callable import voluptuous as vol -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType @@ -74,7 +74,8 @@ async def async_initialize_triggers( triggers = [] for idx, conf in enumerate(trigger_config): platform = await _async_get_trigger_platform(hass, conf) - info = {**info, "trigger_id": f"{idx}"} + trigger_id = conf.get(CONF_ID, f"{idx}") + info = {**info, "trigger_id": trigger_id} triggers.append(platform.async_attach_trigger(hass, conf, action, info)) attach_results = await asyncio.gather(*triggers, return_exceptions=True) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5997be22644..80fe5c52abc 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1405,3 +1405,97 @@ async def test_trigger_service(hass, calls): assert len(calls) == 1 assert calls[0].data.get("trigger") == {"platform": None} assert calls[0].context.parent_id is context.id + + +async def test_trigger_condition_implicit_id(hass, calls): + """Test triggers.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": [ + {"platform": "event", "event_type": "test_event1"}, + {"platform": "event", "event_type": "test_event2"}, + {"platform": "event", "event_type": "test_event3"}, + ], + "action": { + "choose": [ + { + "conditions": {"condition": "trigger", "id": [0, "2"]}, + "sequence": { + "service": "test.automation", + "data": {"param": "one"}, + }, + }, + { + "conditions": {"condition": "trigger", "id": "1"}, + "sequence": { + "service": "test.automation", + "data": {"param": "two"}, + }, + }, + ] + }, + } + }, + ) + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[-1].data.get("param") == "one" + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[-1].data.get("param") == "two" + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[-1].data.get("param") == "one" + + +async def test_trigger_condition_explicit_id(hass, calls): + """Test triggers.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": [ + {"platform": "event", "event_type": "test_event1", "id": "one"}, + {"platform": "event", "event_type": "test_event2", "id": "two"}, + ], + "action": { + "choose": [ + { + "conditions": {"condition": "trigger", "id": "one"}, + "sequence": { + "service": "test.automation", + "data": {"param": "one"}, + }, + }, + { + "conditions": {"condition": "trigger", "id": "two"}, + "sequence": { + "service": "test.automation", + "data": {"param": "two"}, + }, + }, + ] + }, + } + }, + ) + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[-1].data.get("param") == "one" + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[-1].data.get("param") == "two" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index bd5e15ad11f..38a9367e36d 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -2829,3 +2829,17 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, hass_ws_client, c "sun", {"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"}, ) + + +async def test_trigger(hass): + """Test trigger condition.""" + test = await condition.async_from_config( + hass, + {"alias": "Trigger Cond", "condition": "trigger", "id": "123456"}, + ) + + assert not test(hass) + assert not test(hass, {}) + assert not test(hass, {"other_var": "123456"}) + assert not test(hass, {"trigger": {"trigger_id": "123456"}}) + assert test(hass, {"trigger": {"id": "123456"}}) From 5cc31a98e251f750e545b9936e3ceaa6ce7227cb Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Fri, 11 Jun 2021 10:39:57 -0500 Subject: [PATCH 308/750] Add Ecobee humidifier device_info and unique_id (#51504) * Add Ecobee humidifier device_info and unique_id Ecobee humidifier entity was not connected to the thermostat device. This change will ensure the entitiy is properly connected. This change also fills out the ecobee-data.json fixutre data a bit to address failures in the test setup. * Add Ecobee humidifier device_info and unique_id Adjust test fixture data to increase pytest coverage Clean up indenting in ecobee-data.json * Add Ecobee humidifier device_info and unique_id Update exception case in device_info to not be included in codecov tests. This case has been tested locally. * Add Ecobee humidifier device_info and unique_id Address pylint issue * Add Ecobee humidifier device_info and unique_id Remove no cover pragma and add ecobee humidifier.py to .coveragerc --- .coveragerc | 1 + homeassistant/components/ecobee/humidifier.py | 33 +++++++++++++++---- tests/components/ecobee/test_humidifier.py | 3 +- tests/fixtures/ecobee/ecobee-data.json | 33 ++++++++++++++++--- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9437f8943a3..c407c2ab373 100644 --- a/.coveragerc +++ b/.coveragerc @@ -220,6 +220,7 @@ omit = homeassistant/components/ecobee/__init__.py homeassistant/components/ecobee/binary_sensor.py homeassistant/components/ecobee/climate.py + homeassistant/components/ecobee/humidifier.py homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 5067d5080cb..f39fb7acc68 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -10,7 +10,7 @@ from homeassistant.components.humidifier.const import ( SUPPORT_MODES, ) -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SCAN_INTERVAL = timedelta(minutes=3) @@ -43,6 +43,32 @@ class EcobeeHumidifier(HumidifierEntity): self.update_without_throttle = False + @property + def name(self): + """Return the name of the humidifier.""" + return self._name + + @property + def unique_id(self): + """Return unique_id for humidifier.""" + return f"{self.thermostat['identifier']}" + + @property + def device_info(self): + """Return device information for the ecobee humidifier.""" + try: + model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" + except KeyError: + # Ecobee model is not in our list + return None + + return { + "identifiers": {(DOMAIN, self.thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + async def async_update(self): """Get the latest state from the thermostat.""" if self.update_without_throttle: @@ -84,11 +110,6 @@ class EcobeeHumidifier(HumidifierEntity): """Return the current mode, e.g., off, auto, manual.""" return self.thermostat["settings"]["humidifierMode"] - @property - def name(self): - """Return the name of the ecobee thermostat.""" - return self._name - @property def supported_features(self): """Return the list of supported features.""" diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index dd58decfb32..f8a83e4c905 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -27,6 +27,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + STATE_ON, ) from .common import setup_platform @@ -39,7 +40,7 @@ async def test_attributes(hass): await setup_platform(hass, HUMIDIFIER_DOMAIN) state = hass.states.get(DEVICE_ID) - assert state.state == STATE_OFF + assert state.state == STATE_ON assert state.attributes.get(ATTR_MIN_HUMIDITY) == DEFAULT_MIN_HUMIDITY assert state.attributes.get(ATTR_MAX_HUMIDITY) == DEFAULT_MAX_HUMIDITY assert state.attributes.get(ATTR_HUMIDITY) == 40 diff --git a/tests/fixtures/ecobee/ecobee-data.json b/tests/fixtures/ecobee/ecobee-data.json index 2727103c9b1..6e679616085 100644 --- a/tests/fixtures/ecobee/ecobee-data.json +++ b/tests/fixtures/ecobee/ecobee-data.json @@ -1,6 +1,9 @@ { "thermostatList": [ - {"name": "ecobee", + { + "identifier": 8675309, + "name": "ecobee", + "modelNumber": "athenaSmart", "program": { "climates": [ {"name": "Climate1", "climateRef": "c1"}, @@ -9,6 +12,7 @@ "currentClimateRef": "c1" }, "runtime": { + "connected": false, "actualTemperature": 300, "actualHumidity": 15, "desiredHeat": 400, @@ -24,7 +28,7 @@ "heatCoolMinDelta": 50, "holdAction": "nextTransition", "hasHumidifier": true, - "humidifierMode": "off", + "humidifierMode": "manual", "humidity": "30" }, "equipmentStatus": "fan", @@ -37,7 +41,28 @@ "endDate": "2022-01-01 10:00:00", "startDate": "2022-02-02 11:00:00" } - ]} + ], + "remoteSensors": [ + { + "id": "rs:100", + "name": "Remote Sensor 1", + "type": "ecobee3_remote_sensor", + "code": "WKRP", + "inUse": false, + "capability": [ + { + "id": "1", + "type": "temperature", + "value": "782" + }, { + "id": "2", + "type": "occupancy", + "value": "false" + } + ] + } + ] + } ] +} -} \ No newline at end of file From b83b82ca7d04827b4b2cd1210ae023732736ca2e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Jun 2021 20:55:08 +0200 Subject: [PATCH 309/750] WLED WebSocket support - local push updates (#51683) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/wled/__init__.py | 8 + homeassistant/components/wled/const.py | 2 +- homeassistant/components/wled/coordinator.py | 65 ++++- homeassistant/components/wled/manifest.json | 2 +- homeassistant/components/wled/sensor.py | 2 +- homeassistant/components/wled/switch.py | 2 +- tests/components/wled/conftest.py | 1 + tests/components/wled/test_coordinator.py | 196 +++++++++++++ tests/components/wled/test_init.py | 20 ++ tests/fixtures/wled/rgb_websocket.json | 289 +++++++++++++++++++ 10 files changed, 580 insertions(+), 7 deletions(-) create mode 100644 tests/components/wled/test_coordinator.py create mode 100644 tests/fixtures/wled/rgb_websocket.json diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index bd8316a2ff0..2082f45c6c3 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -38,5 +38,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WLED config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + # Ensure disconnected and cleanup stop sub + await coordinator.wled.disconnect() + if coordinator.unsub: + coordinator.unsub() + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 8f759ea3e90..77e404cdd4c 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -6,7 +6,7 @@ import logging DOMAIN = "wled" LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) # Attributes ATTR_COLOR_PRIMARY = "color_primary" diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 8a9312dfc0a..16d56705879 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -1,8 +1,13 @@ """DataUpdateCoordinator for WLED.""" +from __future__ import annotations -from wled import WLED, Device as WLEDDevice, WLEDError +import asyncio +from typing import Callable -from homeassistant.core import HomeAssistant +from wled import WLED, Device as WLEDDevice, WLEDConnectionClosed, WLEDError + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -20,6 +25,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): ) -> None: """Initialize global WLED data updater.""" self.wled = WLED(host, session=async_get_clientsession(hass)) + self.unsub: Callable | None = None super().__init__( hass, @@ -33,9 +39,62 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): for update_callback in self._listeners: update_callback() + @callback + def _use_websocket(self) -> None: + """Use WebSocket for updates, instead of polling.""" + + async def listen() -> None: + """Listen for state changes via WebSocket.""" + try: + await self.wled.connect() + except WLEDError as err: + self.logger.info(err) + if self.unsub: + self.unsub() + self.unsub = None + return + + try: + await self.wled.listen(callback=self.async_set_updated_data) + except WLEDConnectionClosed as err: + self.last_update_success = False + self.logger.info(err) + except WLEDError as err: + self.last_update_success = False + self.update_listeners() + self.logger.error(err) + + # Ensure we are disconnected + await self.wled.disconnect() + if self.unsub: + self.unsub() + self.unsub = None + + async def close_websocket(_) -> None: + """Close WebSocket connection.""" + await self.wled.disconnect() + + # Clean disconnect WebSocket on Home Assistant shutdown + self.unsub = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket + ) + + # Start listening + asyncio.create_task(listen()) + async def _async_update_data(self) -> WLEDDevice: """Fetch data from WLED.""" try: - return await self.wled.update(full_update=not self.last_update_success) + device = await self.wled.update(full_update=not self.last_update_success) except WLEDError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error + + # If the device supports a WebSocket, try activating it. + if ( + device.info.websocket is not None + and not self.wled.connected + and not self.unsub + ): + self._use_websocket() + + return device diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index b5ac91a1bf8..91a449e918d 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,5 +7,5 @@ "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_push" } diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 4008c42f292..37311e333c3 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -40,7 +40,7 @@ async def async_setup_entry( WLEDWifiSignalSensor(coordinator), ] - async_add_entities(sensors, True) + async_add_entities(sensors) class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 74f58472a19..b17572f7607 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -35,7 +35,7 @@ async def async_setup_entry( WLEDSyncSendSwitch(coordinator), WLEDSyncReceiveSwitch(coordinator), ] - async_add_entities(switches, True) + async_add_entities(switches) class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index f66171b6025..80b351a20f1 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -63,6 +63,7 @@ def mock_wled(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None ) as wled_mock: wled = wled_mock.return_value wled.update.return_value = device + wled.connected = False yield wled diff --git a/tests/components/wled/test_coordinator.py b/tests/components/wled/test_coordinator.py new file mode 100644 index 00000000000..47190604238 --- /dev/null +++ b/tests/components/wled/test_coordinator.py @@ -0,0 +1,196 @@ +"""Tests for the coordinator of the WLED integration.""" +import asyncio +from copy import deepcopy +from typing import Callable +from unittest.mock import MagicMock + +import pytest +from wled import ( + Device as WLEDDevice, + WLEDConnectionClosed, + WLEDConnectionError, + WLEDError, +) + +from homeassistant.components.wled.const import SCAN_INTERVAL +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_not_supporting_websocket( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Ensure no WebSocket attempt is made if non-WebSocket device.""" + assert mock_wled.connect.call_count == 0 + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_websocket_already_connected( + hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock +) -> None: + """Ensure no a second WebSocket connection is made, if already connected.""" + assert mock_wled.connect.call_count == 1 + + mock_wled.connected = True + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert mock_wled.connect.call_count == 1 + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_websocket_connect_error_no_listen( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Ensure we don't start listening if WebSocket connection failed.""" + assert mock_wled.connect.call_count == 1 + assert mock_wled.listen.call_count == 1 + + mock_wled.connect.side_effect = WLEDConnectionError + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert mock_wled.connect.call_count == 2 + assert mock_wled.listen.call_count == 1 + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_websocket( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test WebSocket connection.""" + state = hass.states.get("light.wled_websocket") + assert state + assert state.state == STATE_ON + + # There is no Future in place yet... + assert mock_wled.connect.call_count == 1 + assert mock_wled.listen.call_count == 1 + assert mock_wled.disconnect.call_count == 1 + + connection_connected = asyncio.Future() + connection_finished = asyncio.Future() + + async def connect(callback: Callable[[WLEDDevice], None]): + connection_connected.set_result(callback) + await connection_finished + + # Mock out wled.listen with a Future + mock_wled.listen.side_effect = connect + + # Mock out the event bus + mock_bus = MagicMock() + hass.bus = mock_bus + + # Next refresh it should connect + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + callback = await connection_connected + + # Connected to WebSocket, disconnect not called + # listening for Home Assistant to stop + assert mock_wled.connect.call_count == 2 + assert mock_wled.listen.call_count == 2 + assert mock_wled.disconnect.call_count == 1 + assert mock_bus.async_listen_once.call_count == 1 + assert ( + mock_bus.async_listen_once.call_args_list[0][0][0] == EVENT_HOMEASSISTANT_STOP + ) + assert ( + mock_bus.async_listen_once.call_args_list[0][0][1].__name__ == "close_websocket" + ) + assert mock_bus.async_listen_once.return_value.call_count == 0 + + # Send update from WebSocket + updated_device = deepcopy(mock_wled.update.return_value) + updated_device.state.on = False + callback(updated_device) + await hass.async_block_till_done() + + # Check if entity updated + state = hass.states.get("light.wled_websocket") + assert state + assert state.state == STATE_OFF + + # Resolve Future with a connection losed. + connection_finished.set_exception(WLEDConnectionClosed) + await hass.async_block_till_done() + + # Disconnect called, unsubbed Home Assistant stop listener + assert mock_wled.disconnect.call_count == 2 + assert mock_bus.async_listen_once.return_value.call_count == 1 + + # Light still available, as polling takes over + state = hass.states.get("light.wled_websocket") + assert state + assert state.state == STATE_OFF + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_websocket_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test WebSocket connection erroring out, marking lights unavailable.""" + state = hass.states.get("light.wled_websocket") + assert state + assert state.state == STATE_ON + + connection_connected = asyncio.Future() + connection_finished = asyncio.Future() + + async def connect(callback: Callable[[WLEDDevice], None]): + connection_connected.set_result(None) + await connection_finished + + mock_wled.listen.side_effect = connect + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await connection_connected + + # Resolve Future with an error. + connection_finished.set_exception(WLEDError) + await hass.async_block_till_done() + + # Light no longer available as an error occurred + state = hass.states.get("light.wled_websocket") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_websocket_disconnect_on_home_assistant_stop( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Ensure WebSocket is disconnected when Home Assistant stops.""" + assert mock_wled.disconnect.call_count == 1 + connection_connected = asyncio.Future() + connection_finished = asyncio.Future() + + async def connect(callback: Callable[[WLEDDevice], None]): + connection_connected.set_result(None) + await connection_finished + + mock_wled.listen.side_effect = connect + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await connection_connected + + assert mock_wled.disconnect.call_count == 1 + + hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert mock_wled.disconnect.call_count == 2 diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 50ee5520e5d..01821262389 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,6 +1,9 @@ """Tests for the WLED integration.""" +import asyncio +from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch +import pytest from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN @@ -10,19 +13,36 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: AsyncMock ) -> None: """Test the WLED configuration entry unloading.""" + connection_connected = asyncio.Future() + connection_finished = asyncio.Future() + + async def connect(callback: Callable): + connection_connected.set_result(None) + await connection_finished + + # Mock out wled.listen with a Future + mock_wled.listen.side_effect = connect + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + await connection_connected + # Ensure config entry is loaded and are connected assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_wled.connect.call_count == 1 + assert mock_wled.disconnect.call_count == 0 await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() + # Ensure everything is cleaned up nicely and are disconnected + assert mock_wled.disconnect.call_count == 1 assert not hass.data.get(DOMAIN) diff --git a/tests/fixtures/wled/rgb_websocket.json b/tests/fixtures/wled/rgb_websocket.json new file mode 100644 index 00000000000..7e37b489549 --- /dev/null +++ b/tests/fixtures/wled/rgb_websocket.json @@ -0,0 +1,289 @@ +{ + "state": { + "on": true, + "bri": 255, + "transition": 7, + "ps": -1, + "pl": -1, + "ccnf": { + "min": 1, + "max": 5, + "time": 12 + }, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "mode": 1, + "tbri": 0, + "rem": -1 + }, + "udpn": { + "send": false, + "recv": true + }, + "lor": 0, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "len": 13, + "grp": 1, + "spc": 0, + "on": true, + "bri": 255, + "col": [ + [ + 255, + 181, + 218 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ] + ], + "fx": 0, + "sx": 43, + "ix": 128, + "pal": 2, + "sel": true, + "rev": false, + "mi": false + } + ] + }, + "info": { + "ver": "0.12.0-b2", + "vid": 2103220, + "leds": { + "count": 13, + "rgbw": false, + "wv": false, + "pin": [ + 2 + ], + "pwr": 266, + "fps": 2, + "maxpwr": 1000, + "maxseg": 12, + "seglock": false + }, + "str": false, + "name": "WLED WebSocket", + "udpport": 21324, + "live": false, + "lm": "", + "lip": "", + "ws": 0, + "fxcount": 118, + "palcount": 56, + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -68, + "signal": 64, + "channel": 6 + }, + "fs": { + "u": 40, + "t": 1024, + "pmt": 1623156685 + }, + "ndc": 1, + "arch": "esp8266", + "core": "2_7_4_7", + "lwip": 1, + "freeheap": 22752, + "uptime": 258411, + "opt": 127, + "brand": "WLED", + "product": "FOSS", + "mac": "aabbccddeeff" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Scan Dual", + "Fade", + "Theater", + "Theater Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Sparkle Dark", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Strobe Mega", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Aurora", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Tetrix", + "Fire Flicker", + "Gradient", + "Loading", + "Police", + "Police All", + "Two Dots", + "Two Areas", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Scanner Dual", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "Bpm", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkles", + "Lake", + "Meteor", + "Meteor Smooth", + "Railway", + "Ripple", + "Twinklefox", + "Twinklecat", + "Halloween Eyes", + "Solid Pattern", + "Solid Pattern Tri", + "Spots", + "Spots Fade", + "Glitter", + "Candle", + "Fireworks Starburst", + "Fireworks 1D", + "Bouncing Balls", + "Sinelon", + "Sinelon Dual", + "Sinelon Rainbow", + "Popcorn", + "Drip", + "Plasma", + "Percent", + "Ripple Rainbow", + "Heartbeat", + "Pacifica", + "Candle Multi", + "Solid Glitter", + "Sunrise", + "Phased", + "Twinkleup", + "Noise Pal", + "Sine", + "Phased Noise", + "Flow", + "Chunchun", + "Dancing Shadows", + "Washing Machine", + "Candy Cane", + "Blends", + "TV Simulator", + "Dynamic Smooth" + ], + "palettes": [ + "Default", + "* Random Cycle", + "* Color 1", + "* Colors 1&2", + "* Color Gradient", + "* Colors Only", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura", + "Aurora", + "Atlantica", + "C9 2", + "C9 New", + "Temperature", + "Aurora 2" + ] +} \ No newline at end of file From bf6a412be096bdbd602845dbc17ee49c3802c296 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Jun 2021 23:22:18 +0200 Subject: [PATCH 310/750] Tweak device action scaffold, fix typo (#51751) --- .../integration/device_trigger.py | 26 ++++++------------- .../components/climate/test_device_action.py | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 5a060bbfaec..e070bc43f57 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -51,24 +51,14 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: # Add triggers for each entity that belongs to this integration # TODO add your own triggers. - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turned_on", - } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turned_off", - } - ) + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + triggers.append({**base_trigger, CONF_TYPE: "turned_on"}) + triggers.append({**base_trigger, CONF_TYPE: "turned_off"}) return triggers diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index fd5c5548916..3f9b1148443 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -259,7 +259,7 @@ async def test_capabilities( "action,capability_name", [("set_hvac_mode", "hvac_mode"), ("set_preset_mode", "preset_mode")], ) -async def test_capabilities_mising_entity( +async def test_capabilities_missing_entity( hass, device_reg, entity_reg, action, capability_name ): """Test getting capabilities.""" From 30c53a1a13698c2592e91dd63e0305fa82d13f02 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 12 Jun 2021 00:08:54 +0000 Subject: [PATCH 311/750] [ci skip] Translation update --- .../components/ambee/translations/no.json | 19 +++++++++++++++++++ .../ambee/translations/sensor.ca.json | 10 ++++++++++ .../ambee/translations/sensor.et.json | 10 ++++++++++ .../ambee/translations/sensor.nl.json | 10 ++++++++++ .../ambee/translations/sensor.no.json | 10 ++++++++++ .../ambee/translations/sensor.ru.json | 10 ++++++++++ .../ambee/translations/sensor.zh-Hant.json | 10 ++++++++++ 7 files changed, 79 insertions(+) create mode 100644 homeassistant/components/ambee/translations/no.json create mode 100644 homeassistant/components/ambee/translations/sensor.ca.json create mode 100644 homeassistant/components/ambee/translations/sensor.et.json create mode 100644 homeassistant/components/ambee/translations/sensor.nl.json create mode 100644 homeassistant/components/ambee/translations/sensor.no.json create mode 100644 homeassistant/components/ambee/translations/sensor.ru.json create mode 100644 homeassistant/components/ambee/translations/sensor.zh-Hant.json diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json new file mode 100644 index 00000000000..bdfd629ae10 --- /dev/null +++ b/homeassistant/components/ambee/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ca.json b/homeassistant/components/ambee/translations/sensor.ca.json new file mode 100644 index 00000000000..b85d6bdc8e2 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.ca.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Alt", + "low": "Baix", + "moderate": "Moderat", + "very high": "Molt alt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.et.json b/homeassistant/components/ambee/translations/sensor.et.json new file mode 100644 index 00000000000..7599f2fd2c3 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.et.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "K\u00f5rge", + "low": "Madal", + "moderate": "M\u00f5\u00f5dukas", + "very high": "V\u00e4ga k\u00f5rge" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.nl.json b/homeassistant/components/ambee/translations/sensor.nl.json new file mode 100644 index 00000000000..e9ba0c76a34 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.nl.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Hoog", + "low": "Laag", + "moderate": "Matig", + "very high": "Zeer hoog" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.no.json b/homeassistant/components/ambee/translations/sensor.no.json new file mode 100644 index 00000000000..cf4e4bed6ed --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.no.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "H\u00f8y", + "low": "Lav", + "moderate": "Moderat", + "very high": "Veldig h\u00f8y" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ru.json b/homeassistant/components/ambee/translations/sensor.ru.json new file mode 100644 index 00000000000..c0dbe8cecd6 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.ru.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u044b\u0439", + "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.zh-Hant.json b/homeassistant/components/ambee/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..1e3c5bbe58d --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.zh-Hant.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "\u9ad8", + "low": "\u4f4e", + "moderate": "\u4e2d", + "very high": "\u6975\u9ad8" + } + } +} \ No newline at end of file From f6e016554306703c02f9c988caacc863c307f78a Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 12 Jun 2021 10:05:27 +0200 Subject: [PATCH 312/750] Replace garminconnect_aio with garminconnect_ha (#51730) * Fixed config_flow for multiple account creation * Replaced python package to fix multiple accounts * Replaced python package to fix multiple accounts * Implemented config entries user * Config entries user * Fixed test code config flow * Fixed patch --- .../components/garmin_connect/__init__.py | 22 +++-- .../components/garmin_connect/config_flow.py | 8 +- .../components/garmin_connect/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../garmin_connect/test_config_flow.py | 96 +++++++++---------- 6 files changed, 65 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index bc3a1f0aad0..180fcdb08a2 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -2,7 +2,7 @@ from datetime import date import logging -from garminconnect_aio import ( +from garminconnect_ha import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -13,7 +13,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN @@ -26,14 +25,13 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garmin Connect from a config entry.""" - websession = async_get_clientsession(hass) username: str = entry.data[CONF_USERNAME] password: str = entry.data[CONF_PASSWORD] - garmin_client = Garmin(websession, username, password) + api = Garmin(username, password) try: - await garmin_client.login() + await hass.async_add_executor_job(api.login) except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, @@ -49,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.exception("Unknown error occurred during Garmin Connect login request") return False - garmin_data = GarminConnectData(hass, garmin_client) + garmin_data = GarminConnectData(hass, api) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = garmin_data @@ -81,14 +79,20 @@ class GarminConnectData: today = date.today() try: - summary = await self.client.get_user_summary(today.isoformat()) - body = await self.client.get_body_composition(today.isoformat()) + summary = await self.hass.async_add_executor_job( + self.client.get_user_summary, today.isoformat() + ) + body = await self.hass.async_add_executor_job( + self.client.get_body_composition, today.isoformat() + ) self.data = { **summary, **body["totalAverage"], } - self.data["nextAlarm"] = await self.client.get_device_alarms() + self.data["nextAlarm"] = await self.hass.async_add_executor_job( + self.client.get_device_alarms + ) except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index 8f83a9e1071..e9966859f99 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Garmin Connect integration.""" import logging -from garminconnect_aio import ( +from garminconnect_ha import ( Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -38,15 +37,14 @@ class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form() - websession = async_get_clientsession(self.hass) username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - garmin_client = Garmin(websession, username, password) + api = Garmin(username, password) errors = {} try: - await garmin_client.login() + await self.hass.async_add_executor_job(api.login) except GarminConnectConnectionError: errors["base"] = "cannot_connect" return await self._show_setup_form(errors) diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 22e115d0e06..43b4a028290 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,8 +2,8 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect_aio==0.1.4"], + "requirements": ["garminconnect_ha==0.1.6"], "codeowners": ["@cyberjunky"], "config_flow": true, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index fc4912afe04..6b047602c11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -641,7 +641,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.4 +garminconnect_ha==0.1.6 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66422b82287..46881eae733 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ gTTS==2.2.2 garages-amsterdam==2.1.1 # homeassistant.components.garmin_connect -garminconnect_aio==0.1.4 +garminconnect_ha==0.1.6 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 2ad36ffa29c..dd56fba9c1c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect_aio import ( +from garminconnect_ha import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, ) -import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN @@ -21,37 +20,23 @@ MOCK_CONF = { } -@pytest.fixture(name="mock_garmin_connect") -def mock_garmin(): - """Mock Garmin Connect.""" - with patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", - ) as garmin: - garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] - yield garmin.return_value - - async def test_show_form(hass): """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER async def test_step_user(hass): """Test registering an integration and finishing flow works.""" - with patch( - "homeassistant.components.garmin_connect.Garmin.login", - return_value="my@email.address", - ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True - ): + ), patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) @@ -59,60 +44,69 @@ async def test_step_user(hass): assert result["data"] == MOCK_CONF -async def test_connection_error(hass, mock_garmin_connect): +async def test_connection_error(hass): """Test for connection error.""" - mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectConnectionError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} -async def test_authentication_error(hass, mock_garmin_connect): +async def test_authentication_error(hass): """Test for authentication error.""" - mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg") - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectAuthenticationError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} -async def test_toomanyrequest_error(hass, mock_garmin_connect): +async def test_toomanyrequest_error(hass): """Test for toomanyrequests error.""" - mock_garmin_connect.login.side_effect = GarminConnectTooManyRequestsError( - "errormsg" - ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=GarminConnectTooManyRequestsError("errormsg"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "too_many_requests"} -async def test_unknown_error(hass, mock_garmin_connect): +async def test_unknown_error(hass): """Test for unknown error.""" - mock_garmin_connect.login.side_effect = Exception - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) + with patch( + "homeassistant.components.garmin_connect.Garmin.login", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - MockConfigEntry( - domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] - ).add_to_hass(hass) with patch( - "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True - ) as garmin: - garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] - + "homeassistant.components.garmin_connect.config_flow.Garmin", + ): + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 4afe7de07feae49e2de1f583782e8790b784c0d8 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 12 Jun 2021 11:19:05 +0200 Subject: [PATCH 313/750] xknx 0.18.6 (#51758) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 65a74a72518..e9a76a4ef57 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.5"], + "requirements": ["xknx==0.18.6"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 6b047602c11..64a461eff3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2377,7 +2377,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.5 +xknx==0.18.6 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46881eae733..edf437ead77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1286,7 +1286,7 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.5 +xknx==0.18.6 # homeassistant.components.bluesound # homeassistant.components.rest From be137b085b67553aecc542a22f2c2d840bb87da5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 Jun 2021 11:35:33 +0200 Subject: [PATCH 314/750] Refactor zwave_js disconnect client helper (#51718) --- homeassistant/components/zwave_js/__init__.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 3d5d7ab7601..2878580e30c 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -388,7 +388,7 @@ async def async_setup_entry( # noqa: C901 async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" - await disconnect_client(hass, entry, client, listen_task, platform_task) + await disconnect_client(hass, entry) listen_task = asyncio.create_task( client_listen(hass, entry, client, driver_ready) @@ -484,14 +484,12 @@ async def client_listen( hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) -async def disconnect_client( - hass: HomeAssistant, - entry: ConfigEntry, - client: ZwaveClient, - listen_task: asyncio.Task, - platform_task: asyncio.Task, -) -> None: +async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: """Disconnect client.""" + data = hass.data[DOMAIN][entry.entry_id] + client: ZwaveClient = data[DATA_CLIENT] + listen_task: asyncio.Task = data[DATA_CLIENT_LISTEN_TASK] + platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK] listen_task.cancel() platform_task.cancel() platform_setup_tasks = ( @@ -509,7 +507,7 @@ async def disconnect_client( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - info = hass.data[DOMAIN].pop(entry.entry_id) + info = hass.data[DOMAIN][entry.entry_id] for unsub in info[DATA_UNSUBSCRIBE]: unsub() @@ -527,13 +525,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all(await asyncio.gather(*tasks)) if DATA_CLIENT_LISTEN_TASK in info: - await disconnect_client( - hass, - entry, - info[DATA_CLIENT], - info[DATA_CLIENT_LISTEN_TASK], - platform_task=info[DATA_START_PLATFORM_TASK], - ) + await disconnect_client(hass, entry) + + hass.data[DOMAIN].pop(entry.entry_id) if entry.data.get(CONF_USE_ADDON) and entry.disabled_by: addon_manager: AddonManager = get_addon_manager(hass) From 3276666457c985738ffb255fd1e959c4d58433db Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 12 Jun 2021 19:55:32 +1000 Subject: [PATCH 315/750] Bump aio_geojson_nsw_rfs_incidents to v0.4 (#51770) --- .../components/nsw_rural_fire_service_feed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index debc255ec7f..ce75e72f5de 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "nsw_rural_fire_service_feed", "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", - "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"], + "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 64a461eff3f..eabb3562555 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -129,7 +129,7 @@ aio_geojson_geonetnz_quakes==0.12 aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.3 +aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edf437ead77..b48d78b2074 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aio_geojson_geonetnz_quakes==0.12 aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.3 +aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 From c3cfbfe54b26b95fae80a97f7318aa7f1652365f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 Jun 2021 13:12:17 +0200 Subject: [PATCH 316/750] Refactor zwave_js config flow (#51720) --- .../components/zwave_js/config_flow.py | 418 ++++++++++-------- 1 file changed, 236 insertions(+), 182 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index ef39a043b0e..4dd0a084711 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Z-Wave JS integration.""" from __future__ import annotations +from abc import abstractmethod import asyncio import logging from typing import Any @@ -14,7 +15,12 @@ from homeassistant import config_entries, exceptions from homeassistant.components.hassio import is_hassio from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import ( + AbortFlow, + FlowHandler, + FlowManager, + FlowResult, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager @@ -38,7 +44,12 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 4 SERVER_VERSION_TIMEOUT = 10 ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) -STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str}) + + +def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: + """Return a schema for the manual step.""" + default_url = user_input.get(CONF_URL, DEFAULT_URL) + return vol.Schema({vol.Required(CONF_URL, default=default_url): str}) async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: @@ -70,135 +81,24 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Z-Wave JS.""" - - VERSION = 1 +class BaseZwaveJSFlow(FlowHandler): + """Represent the base config flow for Z-Wave JS.""" def __init__(self) -> None: """Set up flow instance.""" self.network_key: str | None = None self.usb_path: str | None = None - self.use_addon = False self.ws_address: str | None = None # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False self.install_task: asyncio.Task | None = None self.start_task: asyncio.Task | None = None + self.version_info: VersionInfo | None = None - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - if is_hassio(self.hass): - return await self.async_step_on_supervisor() - - return await self.async_step_manual() - - async def async_step_manual( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a manual configuration.""" - if user_input is None: - return self.async_show_form( - step_id="manual", data_schema=STEP_USER_DATA_SCHEMA - ) - - errors = {} - - try: - version_info = await validate_input(self.hass, user_input) - except InvalidInput as err: - errors["base"] = err.error - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - await self.async_set_unique_id( - version_info.home_id, raise_on_progress=False - ) - # Make sure we disable any add-on handling - # if the controller is reconfigured in a manual step. - self._abort_if_unique_id_configured( - updates={ - **user_input, - CONF_USE_ADDON: False, - CONF_INTEGRATION_CREATED_ADDON: False, - } - ) - self.ws_address = user_input[CONF_URL] - return self._async_create_entry_from_vars() - - return self.async_show_form( - step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors - ) - - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: - """Receive configuration from add-on discovery info. - - This flow is triggered by the Z-Wave JS add-on. - """ - self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" - try: - version_info = await async_get_version_info(self.hass, self.ws_address) - except CannotConnect: - return self.async_abort(reason="cannot_connect") - - await self.async_set_unique_id(version_info.home_id) - self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address}) - - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Confirm the add-on discovery.""" - if user_input is not None: - return await self.async_step_on_supervisor( - user_input={CONF_USE_ADDON: True} - ) - - return self.async_show_form(step_id="hassio_confirm") - - @callback - def _async_create_entry_from_vars(self) -> FlowResult: - """Return a config entry for the flow.""" - return self.async_create_entry( - title=TITLE, - data={ - CONF_URL: self.ws_address, - CONF_USB_PATH: self.usb_path, - CONF_NETWORK_KEY: self.network_key, - CONF_USE_ADDON: self.use_addon, - CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, - }, - ) - - async def async_step_on_supervisor( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle logic when on Supervisor host.""" - if user_input is None: - return self.async_show_form( - step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA - ) - if not user_input[CONF_USE_ADDON]: - return await self.async_step_manual() - - self.use_addon = True - - addon_info = await self._async_get_addon_info() - - if addon_info.state == AddonState.RUNNING: - addon_config = addon_info.options - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return await self.async_step_finish_addon_setup() - - if addon_info.state == AddonState.NOT_RUNNING: - return await self.async_step_configure_addon() - - return await self.async_step_install_addon() + @property + @abstractmethod + def flow_manager(self) -> FlowManager: + """Return the flow manager of the flow.""" async def async_step_install_addon( self, user_input: dict[str, Any] | None = None @@ -213,10 +113,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.install_task except AddonError as err: + self.install_task = None _LOGGER.error(err) return self.async_show_progress_done(next_step_id="install_failed") self.integration_created_addon = True + self.install_task = None return self.async_show_progress_done(next_step_id="configure_addon") @@ -226,43 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") - async def async_step_configure_addon( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Ask for config for Z-Wave JS add-on.""" - addon_info = await self._async_get_addon_info() - addon_config = addon_info.options - - errors: dict[str, str] = {} - - if user_input is not None: - self.network_key = user_input[CONF_NETWORK_KEY] - self.usb_path = user_input[CONF_USB_PATH] - - new_addon_config = { - CONF_ADDON_DEVICE: self.usb_path, - CONF_ADDON_NETWORK_KEY: self.network_key, - } - - if new_addon_config != addon_config: - await self._async_set_addon_config(new_addon_config) - - return await self.async_step_start_addon() - - usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") - network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "") - - data_schema = vol.Schema( - { - vol.Required(CONF_USB_PATH, default=usb_path): str, - vol.Optional(CONF_NETWORK_KEY, default=network_key): str, - } - ) - - return self.async_show_form( - step_id="configure_addon", data_schema=data_schema, errors=errors - ) - async def async_step_start_addon( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -275,10 +140,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.start_task - except (CannotConnect, AddonError) as err: + except (CannotConnect, AddonError, AbortFlow) as err: + self.start_task = None _LOGGER.error(err) return self.async_show_progress_done(next_step_id="start_failed") + self.start_task = None return self.async_show_progress_done(next_step_id="finish_addon_setup") async def async_step_start_failed( @@ -290,6 +157,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_start_addon(self) -> None: """Start the Z-Wave JS add-on.""" addon_manager: AddonManager = get_addon_manager(self.hass) + self.version_info = None try: await addon_manager.async_schedule_start_addon() # Sleep some seconds to let the add-on start properly before connecting. @@ -301,7 +169,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ws_address = ( f"ws://{discovery_info['host']}:{discovery_info['port']}" ) - await async_get_version_info(self.hass, self.ws_address) + self.version_info = await async_get_version_info( + self.hass, self.ws_address + ) except (AbortFlow, CannotConnect) as err: _LOGGER.debug( "Add-on not ready yet, waiting %s seconds: %s", @@ -315,9 +185,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + self.flow_manager.async_configure(flow_id=self.flow_id) ) + @abstractmethod + async def async_step_configure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ask for config for Z-Wave JS add-on.""" + + @abstractmethod async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -326,27 +203,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Get add-on discovery info and server version info. Set unique id and abort if already configured. """ - if not self.ws_address: - discovery_info = await self._async_get_addon_discovery_info() - self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" - - if not self.unique_id: - try: - version_info = await async_get_version_info(self.hass, self.ws_address) - except CannotConnect as err: - raise AbortFlow("cannot_connect") from err - await self.async_set_unique_id( - version_info.home_id, raise_on_progress=False - ) - - self._abort_if_unique_id_configured( - updates={ - CONF_URL: self.ws_address, - CONF_USB_PATH: self.usb_path, - CONF_NETWORK_KEY: self.network_key, - } - ) - return self._async_create_entry_from_vars() async def _async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" @@ -376,7 +232,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + self.flow_manager.async_configure(flow_id=self.flow_id) ) async def _async_get_addon_discovery_info(self) -> dict: @@ -391,6 +247,204 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return discovery_info_config +class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Z-Wave JS.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up flow instance.""" + super().__init__() + self.use_addon = False + + @property + def flow_manager(self) -> config_entries.ConfigEntriesFlowManager: + """Return the correct flow manager.""" + return self.hass.config_entries.flow + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if is_hassio(self.hass): + return await self.async_step_on_supervisor() + + return await self.async_step_manual() + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a manual configuration.""" + if user_input is None: + return self.async_show_form( + step_id="manual", data_schema=get_manual_schema({}) + ) + + errors = {} + + try: + version_info = await validate_input(self.hass, user_input) + except InvalidInput as err: + errors["base"] = err.error + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + version_info.home_id, raise_on_progress=False + ) + # Make sure we disable any add-on handling + # if the controller is reconfigured in a manual step. + self._abort_if_unique_id_configured( + updates={ + **user_input, + CONF_USE_ADDON: False, + CONF_INTEGRATION_CREATED_ADDON: False, + } + ) + self.ws_address = user_input[CONF_URL] + return self._async_create_entry_from_vars() + + return self.async_show_form( + step_id="manual", data_schema=get_manual_schema(user_input), errors=errors + ) + + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + """Receive configuration from add-on discovery info. + + This flow is triggered by the Z-Wave JS add-on. + """ + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + try: + version_info = await async_get_version_info(self.hass, self.ws_address) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + await self.async_set_unique_id(version_info.home_id) + self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address}) + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm the add-on discovery.""" + if user_input is not None: + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) + + return self.async_show_form(step_id="hassio_confirm") + + async def async_step_on_supervisor( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle logic when on Supervisor host.""" + if user_input is None: + return self.async_show_form( + step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA + ) + if not user_input[CONF_USE_ADDON]: + return await self.async_step_manual() + + self.use_addon = True + + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.RUNNING: + addon_config = addon_info.options + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") + return await self.async_step_finish_addon_setup() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_configure_addon() + + return await self.async_step_install_addon() + + async def async_step_configure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ask for config for Z-Wave JS add-on.""" + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options + + if user_input is not None: + self.network_key = user_input[CONF_NETWORK_KEY] + self.usb_path = user_input[CONF_USB_PATH] + + new_addon_config = { + **addon_config, + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } + + if new_addon_config != addon_config: + await self._async_set_addon_config(new_addon_config) + + return await self.async_step_start_addon() + + usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "") + + data_schema = vol.Schema( + { + vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=network_key): str, + } + ) + + return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + + async def async_step_finish_addon_setup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Prepare info needed to complete the config entry. + + Get add-on discovery info and server version info. + Set unique id and abort if already configured. + """ + if not self.ws_address: + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + + if not self.unique_id: + if not self.version_info: + try: + self.version_info = await async_get_version_info( + self.hass, self.ws_address + ) + except CannotConnect as err: + raise AbortFlow("cannot_connect") from err + + await self.async_set_unique_id( + self.version_info.home_id, raise_on_progress=False + ) + + self._abort_if_unique_id_configured( + updates={ + CONF_URL: self.ws_address, + CONF_USB_PATH: self.usb_path, + CONF_NETWORK_KEY: self.network_key, + } + ) + return self._async_create_entry_from_vars() + + @callback + def _async_create_entry_from_vars(self) -> FlowResult: + """Return a config entry for the flow.""" + return self.async_create_entry( + title=TITLE, + data={ + CONF_URL: self.ws_address, + CONF_USB_PATH: self.usb_path, + CONF_NETWORK_KEY: self.network_key, + CONF_USE_ADDON: self.use_addon, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + }, + ) + + class CannotConnect(exceptions.HomeAssistantError): """Indicate connection error.""" From 779ef3c8e18d684f18d36255122c4b8316dd13a5 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Sat, 12 Jun 2021 13:14:35 +0200 Subject: [PATCH 317/750] Add timedelta option for async_call_later (#50164) --- homeassistant/helpers/event.py | 8 ++++---- tests/helpers/test_event.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 48dd05d2311..85eebf05298 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1214,13 +1214,13 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @bind_hass def async_call_later( hass: HomeAssistant, - delay: float, + delay: float | timedelta, action: HassJob | Callable[..., Awaitable[None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" - return async_track_point_in_utc_time( - hass, action, dt_util.utcnow() + timedelta(seconds=delay) - ) + if not isinstance(delay, timedelta): + delay = timedelta(seconds=delay) + return async_track_point_in_utc_time(hass, action, dt_util.utcnow() + delay) call_later = threaded_listener_factory(async_call_later) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index e134c5e327d..fb7464d405f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3049,6 +3049,27 @@ async def test_async_call_later(hass): assert remove is mock() +async def test_async_call_later_timedelta(hass): + """Test calling an action later with a timedelta.""" + + def action(): + pass + + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch( + "homeassistant.helpers.event.async_track_point_in_utc_time" + ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now): + remove = async_call_later(hass, timedelta(seconds=3), action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert p_hass is hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + assert remove is mock() + + async def test_track_state_change_event_chain_multple_entity(hass): """Test that adding a new state tracker inside a tracker does not fire right away.""" tracker_called = [] From cfce71d7df1eb64bbec567ded11dc52ac7c47417 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Jun 2021 13:33:23 +0200 Subject: [PATCH 318/750] Allow keeping master light in WLED (#51759) --- homeassistant/components/wled/__init__.py | 8 ++++ homeassistant/components/wled/config_flow.py | 45 +++++++++++++++++- homeassistant/components/wled/const.py | 4 ++ homeassistant/components/wled/light.py | 47 +++++++++++++++---- homeassistant/components/wled/strings.json | 9 ++++ .../components/wled/translations/en.json | 9 ++++ tests/components/wled/test_config_flow.py | 25 +++++++++- tests/components/wled/test_light.py | 20 ++++++++ 8 files changed, 155 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 2082f45c6c3..32782338fe5 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -31,6 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up all platforms for this device/entry. hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Reload entry when its updated. + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True @@ -48,3 +51,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN][entry.entry_id] return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload the config entry when it changed.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index bb9d4c0cfe5..7f4d006d122 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -6,13 +6,19 @@ from typing import Any import voluptuous as vol from wled import WLED, WLEDConnectionError -from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow +from homeassistant.config_entries import ( + SOURCE_ZEROCONF, + ConfigEntry, + ConfigFlow, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT, DOMAIN class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): @@ -20,6 +26,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> WLEDOptionsFlowHandler: + """Get the options flow for this handler.""" + return WLEDOptionsFlowHandler(config_entry) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -115,3 +127,32 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): description_placeholders={"name": name}, errors=errors or {}, ) + + +class WLEDOptionsFlowHandler(OptionsFlow): + """Handle WLED options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize WLED options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage WLED options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_KEEP_MASTER_LIGHT, + default=self.config_entry.options.get( + CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT + ), + ): bool, + } + ), + ) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 77e404cdd4c..d80dbf16a60 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -8,6 +8,10 @@ DOMAIN = "wled" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=10) +# Options +CONF_KEEP_MASTER_LIGHT = "keep_master_light" +DEFAULT_KEEP_MASTER_LIGHT = False + # Attributes ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index ed1ec22a65c..c9a074bbdce 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -37,6 +37,8 @@ from .const import ( ATTR_REVERSE, ATTR_SEGMENT_ID, ATTR_SPEED, + CONF_KEEP_MASTER_LIGHT, + DEFAULT_KEEP_MASTER_LIGHT, DOMAIN, SERVICE_EFFECT, SERVICE_PRESET, @@ -84,8 +86,19 @@ async def async_setup_entry( "async_preset", ) + keep_master_light = entry.options.get( + CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT + ) + if keep_master_light: + async_add_entities([WLEDMasterLight(coordinator=coordinator)]) + update_segments = partial( - async_update_segments, entry, coordinator, {}, async_add_entities + async_update_segments, + entry, + coordinator, + keep_master_light, + {}, + async_add_entities, ) coordinator.async_add_listener(update_segments) @@ -169,9 +182,15 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION _attr_icon = "mdi:led-strip-variant" - def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: + def __init__( + self, + coordinator: WLEDDataUpdateCoordinator, + segment: int, + keep_master_light: bool, + ) -> None: """Initialize WLED segment light.""" super().__init__(coordinator=coordinator) + self._keep_master_light = keep_master_light self._rgbw = coordinator.data.info.leds.rgbw self._wv = coordinator.data.info.leds.wv self._segment = segment @@ -247,7 +266,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # If this is the one and only segment, calculate brightness based # on the master and segment brightness - if len(state.segments) == 1: + if not self._keep_master_light and len(state.segments) == 1: return int( (state.segments[self._segment].brightness * state.brightness) / 255 ) @@ -280,7 +299,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) # If there is a single segment, control via the master - if len(self.coordinator.data.state.segments) == 1: + if ( + not self._keep_master_light + and len(self.coordinator.data.state.segments) == 1 + ): await self.coordinator.wled.master(**data) # type: ignore[arg-type] return @@ -313,7 +335,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # When only 1 segment is present, switch along the master, and use # the master for power/brightness control. - if len(self.coordinator.data.state.segments) == 1: + if ( + not self._keep_master_light + and len(self.coordinator.data.state.segments) == 1 + ): master_data = {ATTR_ON: True} if ATTR_BRIGHTNESS in data: master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS] @@ -373,6 +398,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): def async_update_segments( entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, + keep_master_light: bool, current: dict[int, WLEDSegmentLight | WLEDMasterLight], async_add_entities, ) -> None: @@ -383,14 +409,17 @@ def async_update_segments( # Discard master (if present) current_ids.discard(-1) - # Process new segments, add them to Home Assistant new_entities = [] + + # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDSegmentLight(coordinator, segment_id) + current[segment_id] = WLEDSegmentLight( + coordinator, segment_id, keep_master_light + ) new_entities.append(current[segment_id]) # More than 1 segment now? Add master controls - if len(current_ids) < 2 and len(segment_ids) > 1: + if not keep_master_light and (len(current_ids) < 2 and len(segment_ids) > 1): current[-1] = WLEDMasterLight(coordinator) new_entities.append(current[-1]) @@ -404,7 +433,7 @@ def async_update_segments( ) # Remove master if there is only 1 segment left - if len(current_ids) > 1 and len(segment_ids) < 2: + if not keep_master_light and len(current_ids) > 1 and len(segment_ids) < 2: coordinator.hass.async_create_task( async_remove_entity(-1, coordinator, current) ) diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index c42a6cdffb1..9717637fdbb 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -20,5 +20,14 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Keep master light, even with 1 LED segment." + } + } + } } } diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 8ebf6f4d91b..a114d0218ca 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -20,5 +20,14 @@ "title": "Discovered WLED device" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Keep master light, even with 1 LED segment." + } + } + } } } \ No newline at end of file diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index ea0167ed6d5..842e7e332e0 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock from wled import WLEDConnectionError -from homeassistant.components.wled.const import DOMAIN +from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant @@ -177,3 +177,26 @@ async def test_zeroconf_with_mac_device_exists_abort( assert result.get("type") == RESULT_TYPE_ABORT assert result.get("reason") == "already_configured" + + +async def test_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test options config flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_KEEP_MASTER_LIGHT: True}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("data") == { + CONF_KEEP_MASTER_LIGHT: True, + } diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index c37f28a882e..3ce0e167cb5 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -21,6 +21,7 @@ from homeassistant.components.wled.const import ( ATTR_PRESET, ATTR_REVERSE, ATTR_SPEED, + CONF_KEEP_MASTER_LIGHT, DOMAIN, SCAN_INTERVAL, SERVICE_EFFECT, @@ -588,3 +589,22 @@ async def test_preset_service_error( assert "Invalid response from API" in caplog.text assert mock_wled.preset.call_count == 1 mock_wled.preset.assert_called_with(preset=1) + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_single_segment_with_keep_master_light( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the behavior of the integration with a single segment.""" + assert not hass.states.get("light.wled_rgb_light_master") + + hass.config_entries.async_update_entry( + init_integration, options={CONF_KEEP_MASTER_LIGHT: True} + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light_master") + assert state + assert state.state == STATE_ON From c242e56b8c9eb2b4de51aac24406e638cfa843de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Jun 2021 16:18:06 +0200 Subject: [PATCH 319/750] Add re-authentication support to Ambee (#51773) Co-authored-by: Martin Hjelmare --- homeassistant/components/ambee/__init__.py | 46 ++++-- homeassistant/components/ambee/config_flow.py | 47 +++++- homeassistant/components/ambee/strings.json | 9 ++ .../components/ambee/translations/en.json | 9 ++ tests/components/ambee/test_config_flow.py | 147 +++++++++++++++++- tests/components/ambee/test_init.py | 34 +++- 6 files changed, 277 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index 4968420174e..362dc26d851 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -1,12 +1,13 @@ """Support for Ambee.""" from __future__ import annotations -from ambee import Ambee +from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN @@ -24,16 +25,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: longitude=entry.data[CONF_LONGITUDE], ) - for service in {SERVICE_AIR_QUALITY, SERVICE_POLLEN}: - coordinator: DataUpdateCoordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - update_method=getattr(client, service), - ) - await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][service] = coordinator + async def update_air_quality() -> AirQuality: + """Update method for updating Ambee Air Quality data.""" + try: + return await client.air_quality() + except AmbeeAuthenticationError as err: + raise ConfigEntryAuthFailed from err + + air_quality: DataUpdateCoordinator[AirQuality] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_{SERVICE_AIR_QUALITY}", + update_interval=SCAN_INTERVAL, + update_method=update_air_quality, + ) + await air_quality.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][SERVICE_AIR_QUALITY] = air_quality + + async def update_pollen() -> Pollen: + """Update method for updating Ambee Pollen data.""" + try: + return await client.pollen() + except AmbeeAuthenticationError as err: + raise ConfigEntryAuthFailed from err + + pollen: DataUpdateCoordinator[Pollen] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_{SERVICE_POLLEN}", + update_interval=SCAN_INTERVAL, + update_method=update_pollen, + ) + await pollen.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py index 78b25e6226c..0550c541ed0 100644 --- a/homeassistant/components/ambee/config_flow.py +++ b/homeassistant/components/ambee/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from ambee import Ambee, AmbeeAuthenticationError, AmbeeError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -20,6 +20,8 @@ class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + entry: ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -68,3 +70,46 @@ class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle initiation of re-authentication with Ambee.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Ambee.""" + errors = {} + if user_input is not None and self.entry: + session = async_get_clientsession(self.hass) + client = Ambee( + api_key=user_input[CONF_API_KEY], + latitude=self.entry.data[CONF_LATITUDE], + longitude=self.entry.data[CONF_LONGITUDE], + session=session, + ) + try: + await client.air_quality() + except AmbeeAuthenticationError: + errors["base"] = "invalid_api_key" + except AmbeeError: + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json index 8bec71ebe29..e3c306788dd 100644 --- a/homeassistant/components/ambee/strings.json +++ b/homeassistant/components/ambee/strings.json @@ -9,11 +9,20 @@ "longitude": "[%key:common::config_flow::data::longitude%]", "name": "[%key:common::config_flow::data::name%]" } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Ambee account.", + "api_key": "[%key:common::config_flow::data::api_key%]" + } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json index 8728f56fb4e..433580e8023 100644 --- a/homeassistant/components/ambee/translations/en.json +++ b/homeassistant/components/ambee/translations/en.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Re-authentication was successful" + }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key", + "description": "Re-authenticate with your Ambee account." + } + }, "user": { "data": { "api_key": "API Key", diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py index 1f91a842321..a6220418681 100644 --- a/tests/components/ambee/test_config_flow.py +++ b/tests/components/ambee/test_config_flow.py @@ -5,10 +5,16 @@ from unittest.mock import patch from ambee import AmbeeAuthenticationError, AmbeeError from homeassistant.components.ambee.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry async def test_full_user_flow(hass: HomeAssistant) -> None: @@ -127,3 +133,140 @@ async def test_api_error(hass: HomeAssistant) -> None: assert result.get("type") == RESULT_TYPE_FORM assert result.get("errors") == {"base": "cannot_connect"} + + +async def test_reauth_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the reauthentication configuration flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "reauth_confirm" + assert "flow_id" in result + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality" + ) as mock_ambee, patch( + "homeassistant.components.ambee.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "other_key"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_API_KEY: "other_key", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + } + + assert len(mock_ambee.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_with_authentication_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the reauthentication configuration flow with an authentication error. + + This tests tests a reauth flow, with a case the user enters an invalid + API token, but recover by entering the correct one. + """ + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "reauth_confirm" + assert "flow_id" in result + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality", + side_effect=AmbeeAuthenticationError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "invalid", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("errors") == {"base": "invalid_api_key"} + assert "flow_id" in result2 + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality" + ) as mock_ambee, patch( + "homeassistant.components.ambee.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "other_key"}, + ) + await hass.async_block_till_done() + + assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_API_KEY: "other_key", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.44, + } + + assert len(mock_ambee.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_api_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test API error during reauthentication.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert "flow_id" in result + + with patch( + "homeassistant.components.ambee.config_flow.Ambee.air_quality", + side_effect=AmbeeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "invalid", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py index 5db3255dc53..c6ad45735ff 100644 --- a/tests/components/ambee/test_init.py +++ b/tests/components/ambee/test_init.py @@ -2,9 +2,11 @@ from unittest.mock import AsyncMock, MagicMock, patch from ambee import AmbeeConnectionError +from ambee.exceptions import AmbeeAuthenticationError +import pytest from homeassistant.components.ambee.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -44,3 +46,33 @@ async def test_config_entry_not_ready( assert mock_request.call_count == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize("service_name", ["air_quality", "pollen"]) +async def test_config_entry_authentication_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ambee: MagicMock, + service_name: str, +) -> None: + """Test the Ambee configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + + service = getattr(mock_ambee.return_value, service_name) + service.side_effect = AmbeeAuthenticationError + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id From 6ab37881c9cc2d2e879c31b3d08eb59fc78a2339 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Jun 2021 21:31:30 +0200 Subject: [PATCH 320/750] Improve editing of device actions referencing non-added lock (#51750) --- .../components/lock/device_action.py | 11 +- tests/components/lock/test_device_action.py | 101 +++++++----------- 2 files changed, 42 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index 06a133465aa..6c0eb2a41d4 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -17,6 +16,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_supported_features from . import DOMAIN, SUPPORT_OPEN @@ -40,6 +40,8 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue + supported_features = get_supported_features(hass, entry.entity_id) + # Add actions for each entity that belongs to this integration base_action = { CONF_DEVICE_ID: device_id, @@ -50,11 +52,8 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: actions.append({**base_action, CONF_TYPE: "lock"}) actions.append({**base_action, CONF_TYPE: "unlock"}) - state = hass.states.get(entry.entity_id) - if state: - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & (SUPPORT_OPEN): - actions.append({**base_action, CONF_TYPE: "open"}) + if supported_features & (SUPPORT_OPEN): + actions.append({**base_action, CONF_TYPE: "open"}) return actions diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index a84555bdd42..c5a9b19d949 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -2,8 +2,7 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.lock import DOMAIN -from homeassistant.const import CONF_PLATFORM +from homeassistant.components.lock import DOMAIN, SUPPORT_OPEN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -30,15 +29,25 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions_support_open( - hass, device_reg, entity_reg, enable_custom_integrations +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_action_types", + [ + (False, 0, 0, []), + (False, SUPPORT_OPEN, 0, ["open"]), + (True, 0, 0, []), + (True, 0, SUPPORT_OPEN, ["open"]), + ], +) +async def test_get_actions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_action_types, ): - """Test we get the expected actions from a lock which supports open.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - + """Test we get the expected actions from a lock.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -48,69 +57,33 @@ async def test_get_actions_support_open( entity_reg.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["support_open"].unique_id, + "5678", device_id=device_entry.id, + supported_features=features_reg, ) - - expected_actions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + expected_actions = [] + basic_action_types = ["lock", "unlock"] + expected_actions += [ { "domain": DOMAIN, - "type": "lock", + "type": action, "device_id": device_entry.id, - "entity_id": "lock.support_open_lock", - }, - { - "domain": DOMAIN, - "type": "unlock", - "device_id": device_entry.id, - "entity_id": "lock.support_open_lock", - }, - { - "domain": DOMAIN, - "type": "open", - "device_id": device_entry.id, - "entity_id": "lock.support_open_lock", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in basic_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_actions_not_support_open( - hass, device_reg, entity_reg, enable_custom_integrations -): - """Test we get the expected actions from a lock which doesn't support open.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES["no_support_open"].unique_id, - device_id=device_entry.id, - ) - - expected_actions = [ + expected_actions += [ { "domain": DOMAIN, - "type": "lock", + "type": action, "device_id": device_entry.id, - "entity_id": "lock.no_support_open_lock", - }, - { - "domain": DOMAIN, - "type": "unlock", - "device_id": device_entry.id, - "entity_id": "lock.no_support_open_lock", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in expected_action_types ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert_lists_same(actions, expected_actions) From b8669bf4c131b3cdf80b66c97785d69af209d521 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Jun 2021 21:40:32 +0200 Subject: [PATCH 321/750] Improve editing of device actions referencing non-added cover (#51748) --- .../components/cover/device_action.py | 8 +- tests/components/cover/test_device_action.py | 215 +++++------------- 2 files changed, 56 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 24c8a0c8b3b..13ef4523f5b 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -21,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_supported_features from . import ( ATTR_POSITION, @@ -68,11 +68,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: - continue - - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) # Add actions for each entity that belongs to this integration base_action = { diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 60bb4e5401b..0dc76aa7e61 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -2,7 +2,16 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import DOMAIN +from homeassistant.components.cover import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, +) from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -31,56 +40,37 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg, enable_custom_integrations): - """Test we get the expected actions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - - expected_actions = [ - { - "domain": DOMAIN, - "type": "open", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "close", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "stop", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_actions_tilt( - hass, device_reg, entity_reg, enable_custom_integrations +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_action_types", + [ + (False, 0, 0, []), + (False, SUPPORT_CLOSE_TILT, 0, ["close_tilt"]), + (False, SUPPORT_CLOSE, 0, ["close"]), + (False, SUPPORT_OPEN_TILT, 0, ["open_tilt"]), + (False, SUPPORT_OPEN, 0, ["open"]), + (False, SUPPORT_SET_POSITION, 0, ["set_position"]), + (False, SUPPORT_SET_TILT_POSITION, 0, ["set_tilt_position"]), + (False, SUPPORT_STOP, 0, ["stop"]), + (True, 0, 0, []), + (True, 0, SUPPORT_CLOSE_TILT, ["close_tilt"]), + (True, 0, SUPPORT_CLOSE, ["close"]), + (True, 0, SUPPORT_OPEN_TILT, ["open_tilt"]), + (True, 0, SUPPORT_OPEN, ["open"]), + (True, 0, SUPPORT_SET_POSITION, ["set_position"]), + (True, 0, SUPPORT_SET_TILT_POSITION, ["set_tilt_position"]), + (True, 0, SUPPORT_STOP, ["stop"]), + ], +) +async def test_get_actions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_action_types, ): """Test we get the expected actions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[3] - config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -88,124 +78,27 @@ async def test_get_actions_tilt( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) await hass.async_block_till_done() - expected_actions = [ + expected_actions = [] + expected_actions += [ { "domain": DOMAIN, - "type": "open", + "type": action, "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "close", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "stop", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "open_tilt", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "close_tilt", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_actions_set_pos( - hass, device_reg, entity_reg, enable_custom_integrations -): - """Test we get the expected actions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - - expected_actions = [ - { - "domain": DOMAIN, - "type": "set_position", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_actions_set_tilt_pos( - hass, device_reg, entity_reg, enable_custom_integrations -): - """Test we get the expected actions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[2] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - - expected_actions = [ - { - "domain": DOMAIN, - "type": "open", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "close", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "stop", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, - { - "domain": DOMAIN, - "type": "set_tilt_position", - "device_id": device_entry.id, - "entity_id": ent.entity_id, - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in expected_action_types ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert_lists_same(actions, expected_actions) From f362852a24808172697807fa35ff606ede87f34d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Jun 2021 22:07:22 +0200 Subject: [PATCH 322/750] Upgrade black to 21.6b0 (#51785) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8106b2c074f..31d5e9dd16c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 21.5b2 + rev: 21.6b0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a6d8f3f9b04..3f473ae1592 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.5b2 +black==21.6b0 codespell==2.0.0 flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 From f975beae77e956e5d58f7858fa4238431b559a40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Jun 2021 22:23:12 +0200 Subject: [PATCH 323/750] Upgrade wled to 0.6.0 (#51783) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 91a449e918d..8a88cd1e843 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.5.0"], + "requirements": ["wled==0.6.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index eabb3562555..2fb46f87e36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2362,7 +2362,7 @@ wirelesstagpy==0.4.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.5.0 +wled==0.6.0 # homeassistant.components.wolflink wolf_smartset==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b48d78b2074..039f1e9cab7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1277,7 +1277,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.5.0 +wled==0.6.0 # homeassistant.components.wolflink wolf_smartset==0.1.8 From 3a739563b429de23388840ae0a20dfebf1ffdbb0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Jun 2021 23:43:28 +0200 Subject: [PATCH 324/750] Improve editing of device actions referencing non-added alarm (#51747) --- .../alarm_control_panel/device_action.py | 10 +-- .../alarm_control_panel/test_device_action.py | 74 ++++++++++--------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index bb2188807bb..fc218a2c9c3 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_CODE, CONF_DEVICE_ID, CONF_DOMAIN, @@ -23,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType from . import ATTR_CODE_ARM_REQUIRED, DOMAIN @@ -62,13 +62,7 @@ async def async_get_actions( if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - - # We need a state or else we can't populate the HVAC and preset modes. - if state is None: - continue - - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) base_action = { CONF_DEVICE_ID: device_id, diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index a4ec802f3f5..1f66d901603 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -1,7 +1,7 @@ """The tests for Alarm control panel device actions.""" import pytest -from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel import DOMAIN, const import homeassistant.components.automation as automation from homeassistant.const import ( CONF_PLATFORM, @@ -38,7 +38,30 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_action_types", + [ + (False, 0, 0, ["disarm"]), + (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["disarm", "arm_away"]), + (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["disarm", "arm_home"]), + (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["disarm", "arm_night"]), + (False, const.SUPPORT_ALARM_TRIGGER, 0, ["disarm", "trigger"]), + (True, 0, 0, ["disarm"]), + (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["disarm", "arm_away"]), + (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["disarm", "arm_home"]), + (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["disarm", "arm_night"]), + (True, 0, const.SUPPORT_ALARM_TRIGGER, ["disarm", "trigger"]), + ], +) +async def test_get_actions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_action_types, +): """Test we get the expected actions from a alarm_control_panel.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -46,41 +69,26 @@ async def test_get_actions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - expected_actions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + expected_actions = [] + expected_actions += [ { "domain": DOMAIN, - "type": "arm_away", + "type": action, "device_id": device_entry.id, - "entity_id": "alarm_control_panel.test_5678", - }, - { - "domain": DOMAIN, - "type": "arm_home", - "device_id": device_entry.id, - "entity_id": "alarm_control_panel.test_5678", - }, - { - "domain": DOMAIN, - "type": "arm_night", - "device_id": device_entry.id, - "entity_id": "alarm_control_panel.test_5678", - }, - { - "domain": DOMAIN, - "type": "disarm", - "device_id": device_entry.id, - "entity_id": "alarm_control_panel.test_5678", - }, - { - "domain": DOMAIN, - "type": "trigger", - "device_id": device_entry.id, - "entity_id": "alarm_control_panel.test_5678", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in expected_action_types ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert_lists_same(actions, expected_actions) From f9e9202e2de9d2b418b0ee3b5860ef9106136685 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 13 Jun 2021 00:07:25 +0200 Subject: [PATCH 325/750] Improve editing of device triggers referencing non-added alarm (#51701) --- .../alarm_control_panel/device_trigger.py | 10 +- .../test_device_trigger.py | 102 ++++++++++-------- 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index cae9161abf9..f89e03e7326 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -14,7 +14,6 @@ from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -30,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -62,13 +62,7 @@ async def async_get_triggers( if entry.domain != DOMAIN: continue - entity_state = hass.states.get(entry.entity_id) - - # We need a state or else we can't populate the HVAC and preset modes. - if entity_state is None: - continue - - supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) # Add triggers for each entity that belongs to this integration base_trigger = { diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 3380cdd9654..8859915b911 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -48,7 +48,48 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_trigger_types", + [ + (False, 0, 0, ["triggered", "disarmed", "arming"]), + ( + False, + 15, + 0, + [ + "triggered", + "disarmed", + "arming", + "armed_home", + "armed_away", + "armed_night", + ], + ), + (True, 0, 0, ["triggered", "disarmed", "arming"]), + ( + True, + 0, + 15, + [ + "triggered", + "disarmed", + "arming", + "armed_home", + "armed_away", + "armed_night", + ], + ), + ], +) +async def test_get_triggers( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_trigger_types, +): """Test we get the expected triggers from an alarm_control_panel.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -56,53 +97,30 @@ async def test_get_triggers(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - expected_triggers = [ + if set_state: + hass.states.async_set( + "alarm_control_panel.test_5678", + "attributes", + {"supported_features": features_state}, + ) + expected_triggers = [] + + expected_triggers += [ { "platform": "device", "domain": DOMAIN, - "type": "disarmed", + "type": trigger, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "triggered", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "arming", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "armed_home", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "armed_away", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "armed_night", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, + } + for trigger in expected_trigger_types ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, expected_triggers) From 1d0c4c6e99e63a4ae8c4ad02c5e7be63aa0b235a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 13 Jun 2021 00:10:48 +0000 Subject: [PATCH 326/750] [ci skip] Translation update --- .../components/ambee/translations/he.json | 18 +++++++++++++++ .../components/ambee/translations/ru.json | 9 ++++++++ .../ambee/translations/zh-Hant.json | 9 ++++++++ .../modern_forms/translations/he.json | 22 +++++++++++++++++++ .../components/tasmota/translations/he.json | 4 ++++ .../components/wled/translations/et.json | 9 ++++++++ .../components/wled/translations/ru.json | 9 ++++++++ .../components/wled/translations/zh-Hant.json | 9 ++++++++ 8 files changed, 89 insertions(+) create mode 100644 homeassistant/components/ambee/translations/he.json create mode 100644 homeassistant/components/modern_forms/translations/he.json diff --git a/homeassistant/components/ambee/translations/he.json b/homeassistant/components/ambee/translations/he.json new file mode 100644 index 00000000000..3337f1efaef --- /dev/null +++ b/homeassistant/components/ambee/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json index dc585336313..5fb89879a4e 100644 --- a/homeassistant/components/ambee/translations/ru.json +++ b/homeassistant/components/ambee/translations/ru.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambee." + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index b4b37b25102..d2b53af8e5e 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u5bc6\u9470", + "description": "\u91cd\u65b0\u8a8d\u8b49 Ambee \u5e33\u865f\u3002" + } + }, "user": { "data": { "api_key": "API \u5bc6\u9470", diff --git a/homeassistant/components/modern_forms/translations/he.json b/homeassistant/components/modern_forms/translations/he.json new file mode 100644 index 00000000000..701a3689598 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json index 16e1fafbdc8..7853c226b33 100644 --- a/homeassistant/components/tasmota/translations/he.json +++ b/homeassistant/components/tasmota/translations/he.json @@ -5,7 +5,11 @@ }, "step": { "config": { + "description": "\u05d0\u05e0\u05d0 \u05d4\u05db\u05e0\u05e1 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea Tasmota.", "title": "Tasmota" + }, + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Tasmota?" } } } diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json index 4771c0b3af4..b3fdec52961 100644 --- a/homeassistant/components/wled/translations/et.json +++ b/homeassistant/components/wled/translations/et.json @@ -20,5 +20,14 @@ "title": "Leitud WLED seade" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Kasuta p\u00f5hivalgust isegi \u00fche LED-segmendi korral." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 1aefafca5f1..b43013b34e7 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -20,5 +20,14 @@ "title": "WLED" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "\u0414\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0432\u0435\u0442 \u0434\u0430\u0436\u0435 \u0441 \u043e\u0434\u043d\u0438\u043c \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u043c \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u043c." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 0980bcf59aa..b8c873b90a5 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -20,5 +20,14 @@ "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "\u4fdd\u7559\u4e3b\u71c8\u5149\u3001\u5373\u4fbf\u50c5\u5269 1 \u6bb5 LED\u3002" + } + } + } } } \ No newline at end of file From ae28e4934f6d1cdbbfd93ac1d3300e49fc1bb615 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Jun 2021 07:41:27 +0200 Subject: [PATCH 327/750] Mark Ambee as a platinum quality integration (#51779) --- homeassistant/components/ambee/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index 7293f8ea4b4..e546f5009e8 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ambee", "requirements": ["ambee==0.3.0"], "codeowners": ["@frenck"], + "quality_scale": "platinum", "iot_class": "cloud_polling" } From 33ac4dba5ab8c01c4ab3c1d9626e6ce70910c98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 13 Jun 2021 10:21:26 +0200 Subject: [PATCH 328/750] Add httpcore with version 0.13.3 (#51799) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 85a06e43413..60ae7617702 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -62,3 +62,7 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# httpcore 0.13.4 breaks several integrations +# https://github.com/home-assistant/core/issues/51778 +httpcore==0.13.3 + diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4fd96cb1b04..dc0c5fa2c10 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -83,6 +83,10 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 +# httpcore 0.13.4 breaks several integrations +# https://github.com/home-assistant/core/issues/51778 +httpcore==0.13.3 + """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 936f155499334676027f9235011eb52893d94a72 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sun, 13 Jun 2021 11:38:55 +0200 Subject: [PATCH 329/750] Bump pydaikin, fix airbase issues (#51797) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 2db81e8f167..ec0b2716053 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.1"], + "requirements": ["pydaikin==2.4.2"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 2fb46f87e36..9660bfc9647 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1357,7 +1357,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.1 +pydaikin==2.4.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 039f1e9cab7..a708bc19487 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -752,7 +752,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.1 +pydaikin==2.4.2 # homeassistant.components.deconz pydeconz==79 From aacb334cc80286bdfe75d70392865a1a17303897 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 13 Jun 2021 12:05:09 +0200 Subject: [PATCH 330/750] Remove connection classes (#51801) --- homeassistant/components/growatt_server/config_flow.py | 1 - homeassistant/components/kraken/config_flow.py | 1 - homeassistant/components/modern_forms/config_flow.py | 7 +------ homeassistant/components/synology_dsm/config_flow.py | 8 +------- homeassistant/components/system_bridge/config_flow.py | 1 - 5 files changed, 2 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index 300c96746e7..cc1457d3687 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -13,7 +13,6 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow class.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialise growatt server flow.""" diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index a619fb1e962..68443705767 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -24,7 +24,6 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for kraken.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index 67eb9cef0e4..e8b557f7bc5 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -6,11 +6,7 @@ from typing import Any from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice import voluptuous as vol -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_POLL, - SOURCE_ZEROCONF, - ConfigFlow, -) +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -23,7 +19,6 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a ModernForms config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index c2de05b833e..1a3681daf32 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -17,12 +17,7 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.components import ssdp -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_POLL, - ConfigEntry, - ConfigFlow, - OptionsFlow, -) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -92,7 +87,6 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index a74c060fccf..a93420bf6ae 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -65,7 +65,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for System Bridge.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize flow.""" From 49a943cc940e45a6db51103f1844f83710a6cb61 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 13 Jun 2021 12:05:24 +0200 Subject: [PATCH 331/750] Fix Roomba strings step_id rename (#51744) --- homeassistant/components/roomba/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 867d2bf633f..1a37745302a 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -2,7 +2,7 @@ "config": { "flow_title": "{name} ({host})", "step": { - "init": { + "user": { "title": "Automatically connect to the device", "description": "Select a Roomba or Braava.", "data": { @@ -16,7 +16,7 @@ "host": "[%key:common::config_flow::data::host%]", "blid": "BLID" } - }, + }, "link": { "title": "Retrieve Password", "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." @@ -27,7 +27,7 @@ "data": { "password": "[%key:common::config_flow::data::password%]" } - } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" @@ -37,7 +37,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "not_irobot_device": "Discovered device is not an iRobot device", "short_blid": "The BLID was truncated" - } + } }, "options": { "step": { From a31e6716d968b05b077428029530dcd894011d84 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 13 Jun 2021 17:34:42 +0300 Subject: [PATCH 332/750] Cleanup switcher_kis - move to consts (#51807) --- .../components/switcher_kis/__init__.py | 23 ++++++--------- .../components/switcher_kis/const.py | 28 +++++++++++++++++++ .../components/switcher_kis/switch.py | 24 +++++----------- 3 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/switcher_kis/const.py diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 5483ad88c2d..6627e9d097c 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,4 +1,4 @@ -"""Home Assistant Switcher Component.""" +"""The Switcher integration.""" from __future__ import annotations from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for @@ -17,21 +17,16 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import EventType +from .const import ( + CONF_DEVICE_PASSWORD, + CONF_PHONE_ID, + DATA_DEVICE, + DOMAIN, + SIGNAL_SWITCHER_DEVICE_UPDATE, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "switcher_kis" - -CONF_DEVICE_PASSWORD = "device_password" -CONF_PHONE_ID = "phone_id" - -DATA_DEVICE = "device" - -SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update" - -ATTR_AUTO_OFF_SET = "auto_off_set" -ATTR_ELECTRIC_CURRENT = "electric_current" -ATTR_REMAINING_TIME = "remaining_time" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py new file mode 100644 index 00000000000..da51fae4d1a --- /dev/null +++ b/homeassistant/components/switcher_kis/const.py @@ -0,0 +1,28 @@ +"""Constants for the Switcher integration.""" +from homeassistant.components.switch import ATTR_CURRENT_POWER_W + +DOMAIN = "switcher_kis" + +CONF_DEVICE_PASSWORD = "device_password" +CONF_PHONE_ID = "phone_id" + +DATA_DEVICE = "device" + +SIGNAL_SWITCHER_DEVICE_UPDATE = "switcher_device_update" + +ATTR_AUTO_OFF_SET = "auto_off_set" +ATTR_ELECTRIC_CURRENT = "electric_current" +ATTR_REMAINING_TIME = "remaining_time" + +CONF_AUTO_OFF = "auto_off" +CONF_TIMER_MINUTES = "timer_minutes" + +DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { + "power_consumption": ATTR_CURRENT_POWER_W, + "electric_current": ATTR_ELECTRIC_CURRENT, + "remaining_time": ATTR_REMAINING_TIME, + "auto_off_set": ATTR_AUTO_OFF_SET, +} + +SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" +SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 8f7332162a9..343d4dd8b13 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -13,37 +13,27 @@ from aioswitcher.consts import ( from aioswitcher.devices import SwitcherV2Device import voluptuous as vol -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - ATTR_AUTO_OFF_SET, - ATTR_ELECTRIC_CURRENT, - ATTR_REMAINING_TIME, +from .const import ( + CONF_AUTO_OFF, + CONF_TIMER_MINUTES, DATA_DEVICE, + DEVICE_PROPERTIES_TO_HA_ATTRIBUTES, DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_TURN_ON_WITH_TIMER_NAME, SIGNAL_SWITCHER_DEVICE_UPDATE, ) -CONF_AUTO_OFF = "auto_off" -CONF_TIMER_MINUTES = "timer_minutes" - -DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { - "power_consumption": ATTR_CURRENT_POWER_W, - "electric_current": ATTR_ELECTRIC_CURRENT, - "remaining_time": ATTR_REMAINING_TIME, - "auto_off_set": ATTR_AUTO_OFF_SET, -} - -SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_SET_AUTO_OFF_SCHEMA = { vol.Required(CONF_AUTO_OFF): cv.time_period_str, } -SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { vol.Required(CONF_TIMER_MINUTES): vol.All( cv.positive_int, vol.Range(min=1, max=150) From fbe507a9c1a8479d3abdffc2283017e88e2cc230 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 13 Jun 2021 16:45:35 +0200 Subject: [PATCH 333/750] Strict types - first part (#51479) --- homeassistant/components/fritz/__init__.py | 6 +-- .../components/fritz/binary_sensor.py | 9 ++-- .../components/fritz/device_tracker.py | 54 +++++++++++-------- homeassistant/components/fritz/services.py | 10 ++-- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 35e924c807c..6d0030685b2 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .common import FritzBoxTools, FritzData @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_FRITZ] = FritzData() @callback - def _async_unload(event): + def _async_unload(event: Event) -> None: fritz_tools.async_unload() entry.async_on_unload( @@ -83,7 +83,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update when config_entry options update.""" if entry.options: await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index bc8fa204ee5..2154a397584 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import FritzBoxBaseEntity, FritzBoxTools from .const import DOMAIN @@ -17,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up entry.""" _LOGGER.debug("Setting up FRITZ!Box binary sensors") @@ -44,12 +45,12 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): super().__init__(fritzbox_tools, device_friendly_name) @property - def name(self): + def name(self) -> str: """Return name.""" return self._name @property - def device_class(self): + def device_class(self) -> str: """Return device class.""" return DEVICE_CLASS_CONNECTIVITY @@ -59,7 +60,7 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): return self._is_on @property - def unique_id(self): + def unique_id(self) -> str: """Return unique id.""" return self._unique_id diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index dbf6bc0df93..0e75a781c5d 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -17,9 +17,11 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from .common import FritzBoxTools, FritzDevice +from .common import Device, FritzBoxTools, FritzData, FritzDevice from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -41,7 +43,7 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_get_scanner(hass: HomeAssistant, config: ConfigType): +async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> None: """Import legacy FRITZ!Box configuration.""" _LOGGER.debug("Import legacy FRITZ!Box configuration from YAML") @@ -63,15 +65,15 @@ async def async_get_scanner(hass: HomeAssistant, config: ConfigType): async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for FRITZ!Box component.""" _LOGGER.debug("Starting FRITZ!Box device tracker") - router = hass.data[DOMAIN][entry.entry_id] - data_fritz = hass.data[DATA_FRITZ] + router: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + data_fritz: FritzData = hass.data[DATA_FRITZ] @callback - def update_router(): + def update_router() -> None: """Update the values of the router.""" _async_add_entities(router, async_add_entities, data_fritz) @@ -83,10 +85,14 @@ async def async_setup_entry( @callback -def _async_add_entities(router, async_add_entities, data_fritz): +def _async_add_entities( + router: FritzBoxTools, + async_add_entities: AddEntitiesCallback, + data_fritz: FritzData, +) -> None: """Add new tracker entities from the router.""" - def _is_tracked(mac, device): + def _is_tracked(mac: str, device: Device) -> bool: for tracked in data_fritz.tracked.values(): if mac in tracked: return True @@ -118,27 +124,28 @@ class FritzBoxTracker(ScannerEntity): self._name = device.hostname or DEFAULT_DEVICE_NAME self._last_activity = device.last_activity self._active = False - self._attrs: dict = {} @property - def is_connected(self): + def is_connected(self) -> bool: """Return device status.""" return self._active @property - def name(self): + def name(self) -> str: """Return device name.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return device unique id.""" return self._mac @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" - return self._router.devices[self._mac].ip_address + if self._mac: + return self._router.devices[self._mac].ip_address + return None @property def mac_address(self) -> str: @@ -146,9 +153,11 @@ class FritzBoxTracker(ScannerEntity): return self._mac @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return hostname of the device.""" - return self._router.devices[self._mac].hostname + if self._mac: + return self._router.devices[self._mac].hostname + return None @property def source_type(self) -> str: @@ -156,7 +165,7 @@ class FritzBoxTracker(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, @@ -176,7 +185,7 @@ class FritzBoxTracker(ScannerEntity): return False @property - def icon(self): + def icon(self) -> str: """Return device icon.""" if self.is_connected: return "mdi:lan-connect" @@ -200,17 +209,20 @@ class FritzBoxTracker(ScannerEntity): @callback def async_process_update(self) -> None: """Update device.""" - device: FritzDevice = self._router.devices[self._mac] + if not self._mac: + return + + device = self._router.devices[self._mac] self._active = device.is_connected self._last_activity = device.last_activity @callback - def async_on_demand_update(self): + def async_on_demand_update(self) -> None: """Update state.""" self.async_process_update() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self.async_process_update() self.async_on_remove( diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index 7ed5ecd3c40..fcfbd54b743 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -10,14 +10,14 @@ from .const import DOMAIN, FRITZ_SERVICES, SERVICE_REBOOT, SERVICE_RECONNECT _LOGGER = logging.getLogger(__name__) -async def async_setup_services(hass: HomeAssistant): +async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" for service in [SERVICE_REBOOT, SERVICE_RECONNECT]: if hass.services.has_service(DOMAIN, service): return - async def async_call_fritz_service(service_call): + async def async_call_fritz_service(service_call: ServiceCall) -> None: """Call correct Fritz service.""" if not ( @@ -40,10 +40,10 @@ async def async_setup_services(hass: HomeAssistant): async def _async_get_configured_fritz_tools( hass: HomeAssistant, service_call: ServiceCall -): +) -> list: """Get FritzBoxTools class from config entry.""" - list_entry_id = [] + list_entry_id: list = [] for entry_id in await async_extract_config_entry_ids(hass, service_call): config_entry = hass.config_entries.async_get_entry(entry_id) if config_entry and config_entry.domain == DOMAIN: @@ -51,7 +51,7 @@ async def _async_get_configured_fritz_tools( return list_entry_id -async def async_unload_services(hass: HomeAssistant): +async def async_unload_services(hass: HomeAssistant) -> None: """Unload services for Fritz integration.""" if not hass.data.get(FRITZ_SERVICES): From 1adeb82930001305185a9650f5016a55eff84ae0 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 13 Jun 2021 09:22:20 -0700 Subject: [PATCH 334/750] Bump androidtv to 0.0.60 (#51812) * Bump androidtv to 0.0.60 * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 9ab02fec68a..f338ac4683e 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.3.1", - "androidtv[async]==0.0.59", + "androidtv[async]==0.0.60", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"], diff --git a/requirements_all.txt b/requirements_all.txt index 9660bfc9647..cd3d505a928 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,7 +263,7 @@ ambiclimate==0.2.1 amcrest==1.7.2 # homeassistant.components.androidtv -androidtv[async]==0.0.59 +androidtv[async]==0.0.60 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a708bc19487..99aafe4f418 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ ambee==0.3.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.59 +androidtv[async]==0.0.60 # homeassistant.components.apns apns2==0.3.0 From 123e8f01a19ec075b597f1052245a24c89388bf6 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 14 Jun 2021 00:41:21 +0800 Subject: [PATCH 335/750] Refactor stream to create partial segments (#51282) --- homeassistant/components/stream/const.py | 3 +- homeassistant/components/stream/core.py | 29 ++- homeassistant/components/stream/fmp4utils.py | 10 - homeassistant/components/stream/hls.py | 86 +++++---- homeassistant/components/stream/recorder.py | 2 +- homeassistant/components/stream/worker.py | 174 +++++++++++------ tests/components/stream/conftest.py | 62 ++++++ tests/components/stream/test_hls.py | 141 ++++++-------- tests/components/stream/test_recorder.py | 107 +++-------- tests/components/stream/test_worker.py | 190 +++++++++++++++---- 10 files changed, 499 insertions(+), 305 deletions(-) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 62d13321f91..cf4a80d9705 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -18,8 +18,9 @@ FORMAT_CONTENT_TYPE = {HLS_PROVIDER: "application/vnd.apple.mpegurl"} OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist -MAX_SEGMENTS = 4 # Max number of segments to keep around +MAX_SEGMENTS = 5 # Max number of segments to keep around TARGET_SEGMENT_DURATION = 2.0 # Each segment is about this many seconds +TARGET_PART_DURATION = 1.0 SEGMENT_DURATION_ADJUSTER = 0.1 # Used to avoid missing keyframe boundaries # Each segment is at least this many seconds MIN_SEGMENT_DURATION = TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index f3d30fa6e1b..136c3c1dbfa 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -19,20 +19,37 @@ from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() +@attr.s(slots=True) +class Part: + """Represent a segment part.""" + + duration: float = attr.ib() + has_keyframe: bool = attr.ib() + data: bytes = attr.ib() + + @attr.s(slots=True) class Segment: """Represent a segment.""" - sequence: int = attr.ib() - # the init of the mp4 - init: bytes = attr.ib() - # the video data (moof + mddat)s of the mp4 - moof_data: bytes = attr.ib() - duration: float = attr.ib() + sequence: int = attr.ib(default=0) + # the init of the mp4 the segment is based on + init: bytes = attr.ib(default=None) + duration: float = attr.ib(default=0) # For detecting discontinuities across stream restarts stream_id: int = attr.ib(default=0) + parts: list[Part] = attr.ib(factory=list) start_time: datetime.datetime = attr.ib(factory=datetime.datetime.utcnow) + @property + def complete(self) -> bool: + """Return whether the Segment is complete.""" + return self.duration > 0 + + def get_bytes_without_init(self) -> bytes: + """Return reconstructed data for entire segment as bytes.""" + return b"".join([part.data for part in self.parts]) + class IdleTimer: """Invoke a callback after an inactivity timeout. diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 511bbc0939a..ef01158be62 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -25,16 +25,6 @@ def find_box( index += int.from_bytes(box_header[0:4], byteorder="big") -def get_init_and_moof_data(segment: memoryview) -> tuple[bytes, bytes]: - """Get the init and moof data from a segment.""" - moof_location = next(find_box(segment, b"moof"), 0) - mfra_location = next(find_box(segment, b"mfra"), len(segment)) - return ( - segment[:moof_location].tobytes(), - segment[moof_location:mfra_location].tobytes(), - ) - - def get_codec_string(mp4_bytes: bytes) -> str: """Get RFC 6381 codec string.""" codecs = [] diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 1d2921df192..0b0cd4ac3b2 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -37,9 +37,12 @@ class HlsMasterPlaylistView(StreamView): # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work # Calculate file size / duration and use a small multiplier to account for variation # hls spec already allows for 25% variation - segment = track.get_segment(track.sequences[-1]) + segment = track.get_segment(track.sequences[-2]) bandwidth = round( - (len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2 + (len(segment.init) + sum(len(part.data) for part in segment.parts)) + * 8 + / segment.duration + * 1.2 ) codecs = get_codec_string(segment.init) lines = [ @@ -53,9 +56,11 @@ class HlsMasterPlaylistView(StreamView): """Return m3u8 playlist.""" track = stream.add_provider(HLS_PROVIDER) stream.start() - # Wait for a segment to be ready + # Make sure at least two segments are ready (last one may not be complete) if not track.sequences and not await track.recv(): return web.HTTPNotFound() + if len(track.sequences) == 1 and not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -68,69 +73,72 @@ class HlsPlaylistView(StreamView): cors_allowed = True @staticmethod - def render_preamble(track): - """Render preamble.""" - return [ - "#EXT-X-VERSION:6", - f"#EXT-X-TARGETDURATION:{track.target_duration}", - '#EXT-X-MAP:URI="init.mp4"', - ] - - @staticmethod - def render_playlist(track): + def render(track): """Render playlist.""" - segments = list(track.get_segments())[-NUM_PLAYLIST_SEGMENTS:] + # NUM_PLAYLIST_SEGMENTS+1 because most recent is probably not yet complete + segments = list(track.get_segments())[-(NUM_PLAYLIST_SEGMENTS + 1) :] - if not segments: - return [] + # To cap the number of complete segments at NUM_PLAYLIST_SEGMENTS, + # remove the first segment if the last segment is actually complete + if segments[-1].complete: + segments = segments[-NUM_PLAYLIST_SEGMENTS:] first_segment = segments[0] playlist = [ + "#EXTM3U", + "#EXT-X-VERSION:6", + "#EXT-X-INDEPENDENT-SEGMENTS", + '#EXT-X-MAP:URI="init.mp4"', + f"#EXT-X-TARGETDURATION:{track.target_duration:.0f}", f"#EXT-X-MEDIA-SEQUENCE:{first_segment.sequence}", f"#EXT-X-DISCONTINUITY-SEQUENCE:{first_segment.stream_id}", "#EXT-X-PROGRAM-DATE-TIME:" + first_segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", # Since our window doesn't have many segments, we don't want to start - # at the beginning or we risk a behind live window exception in exoplayer. + # at the beginning or we risk a behind live window exception in Exoplayer. # EXT-X-START is not supposed to be within 3 target durations of the end, - # but this seems ok - f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f},PRECISE=YES", + # but a value as low as 1.5 doesn't seem to hurt. + # A value below 3 may not be as useful for hls.js as many hls.js clients + # don't autoplay. Also, hls.js uses the player parameter liveSyncDuration + # which seems to take precedence for setting target delay. Yet it also + # doesn't seem to hurt, so we can stick with it for now. + f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f}", ] last_stream_id = first_segment.stream_id + # Add playlist sections for segment in segments: - if last_stream_id != segment.stream_id: + # Skip last segment if it is not complete + if segment.complete: + if last_stream_id != segment.stream_id: + playlist.extend( + [ + "#EXT-X-DISCONTINUITY", + "#EXT-X-PROGRAM-DATE-TIME:" + + segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + + "Z", + ] + ) playlist.extend( [ - "#EXT-X-DISCONTINUITY", - "#EXT-X-PROGRAM-DATE-TIME:" - + segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] - + "Z", + f"#EXTINF:{segment.duration:.3f},", + f"./segment/{segment.sequence}.m4s", ] ) - playlist.extend( - [ - f"#EXTINF:{float(segment.duration):.04f},", - f"./segment/{segment.sequence}.m4s", - ] - ) - last_stream_id = segment.stream_id + last_stream_id = segment.stream_id - return playlist - - def render(self, track): - """Render M3U8 file.""" - lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track) - return "\n".join(lines) + "\n" + return "\n".join(playlist) + "\n" async def handle(self, request, stream, sequence): """Return m3u8 playlist.""" track = stream.add_provider(HLS_PROVIDER) stream.start() - # Wait for a segment to be ready + # Make sure at least two segments are ready (last one may not be complete) if not track.sequences and not await track.recv(): return web.HTTPNotFound() + if len(track.sequences) == 1 and not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]} response = web.Response( body=self.render(track).encode("utf-8"), headers=headers @@ -170,7 +178,7 @@ class HlsSegmentView(StreamView): return web.HTTPNotFound() headers = {"Content-Type": "video/iso.segment"} return web.Response( - body=segment.moof_data, + body=segment.get_bytes_without_init(), headers=headers, ) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index ac5f102e625..8e21777fa0b 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -57,7 +57,7 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]): # Open segment source = av.open( - BytesIO(segment.init + segment.moof_data), + BytesIO(segment.init + segment.get_bytes_without_init()), "r", format=SEGMENT_CONTAINER_FORMAT, ) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index c606d1ad0dc..cca981e5db3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -2,9 +2,12 @@ from __future__ import annotations from collections import deque +from collections.abc import Iterator, Mapping +from fractions import Fraction from io import BytesIO import logging -from typing import cast +from threading import Event +from typing import Callable, cast import av @@ -17,9 +20,9 @@ from .const import ( PACKETS_TO_WAIT_FOR_AUDIO, SEGMENT_CONTAINER_FORMAT, SOURCE_TIMEOUT, + TARGET_PART_DURATION, ) -from .core import Segment, StreamOutput -from .fmp4utils import get_init_and_moof_data +from .core import Part, Segment, StreamOutput _LOGGER = logging.getLogger(__name__) @@ -27,22 +30,28 @@ _LOGGER = logging.getLogger(__name__) class SegmentBuffer: """Buffer for writing a sequence of packets to the output as a segment.""" - def __init__(self, outputs_callback) -> None: + def __init__( + self, outputs_callback: Callable[[], Mapping[str, StreamOutput]] + ) -> None: """Initialize SegmentBuffer.""" - self._stream_id = 0 - self._outputs_callback = outputs_callback - self._outputs: list[StreamOutput] = [] + self._stream_id: int = 0 + self._outputs_callback: Callable[ + [], Mapping[str, StreamOutput] + ] = outputs_callback # sequence gets incremented before the first segment so the first segment # has a sequence number of 0. self._sequence = -1 - self._segment_start_pts = None + self._segment_start_dts: int = cast(int, None) self._memory_file: BytesIO = cast(BytesIO, None) self._av_output: av.container.OutputContainer = None self._input_video_stream: av.video.VideoStream = None self._input_audio_stream = None # av.audio.AudioStream | None self._output_video_stream: av.video.VideoStream = None self._output_audio_stream = None # av.audio.AudioStream | None - self._segment: Segment = cast(Segment, None) + self._segment: Segment | None = None + self._segment_last_write_pos: int = cast(int, None) + self._part_start_dts: int = cast(int, None) + self._part_has_keyframe = False @staticmethod def make_new_av( @@ -56,10 +65,17 @@ class SegmentBuffer: container_options={ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 # "cmaf" flag replaces several of the movflags used, but too recent to use for now - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", - "avoid_negative_ts": "disabled", + "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + # Sometimes the first segment begins with negative timestamps, and this setting just + # adjusts the timestamps in the output from that segment to start from 0. Helps from + # having to make some adjustments in test_durations + "avoid_negative_ts": "make_non_negative", "fragment_index": str(sequence + 1), "video_track_timescale": str(int(1 / input_vstream.time_base)), + # Create a fragments every TARGET_PART_DURATION. The data from each fragment is stored in + # a "Part" that can be combined with the data from all the other "Part"s, plus an init + # section, to reconstitute the data in a "Segment". + "frag_duration": str(int(TARGET_PART_DURATION * 1e6)), }, ) @@ -73,15 +89,13 @@ class SegmentBuffer: self._input_video_stream = video_stream self._input_audio_stream = audio_stream - def reset(self, video_pts): + def reset(self, video_dts: int) -> None: """Initialize a new stream segment.""" # Keep track of the number of segments we've processed self._sequence += 1 - self._segment_start_pts = video_pts - - # Fetch the latest StreamOutputs, which may have changed since the - # worker started. - self._outputs = self._outputs_callback().values() + self._segment_start_dts = self._part_start_dts = video_dts + self._segment = None + self._segment_last_write_pos = 0 self._memory_file = BytesIO() self._av_output = self.make_new_av( memory_file=self._memory_file, @@ -98,54 +112,102 @@ class SegmentBuffer: template=self._input_audio_stream ) - def mux_packet(self, packet): + def mux_packet(self, packet: av.Packet) -> None: """Mux a packet to the appropriate output stream.""" # Check for end of segment - if packet.stream == self._input_video_stream and packet.is_keyframe: - duration = (packet.pts - self._segment_start_pts) * packet.time_base - if duration >= MIN_SEGMENT_DURATION: - # Save segment to outputs - self.flush(duration) - - # Reinitialize - self.reset(packet.pts) - - # Mux the packet if packet.stream == self._input_video_stream: + + if ( + packet.is_keyframe + and ( + segment_duration := (packet.dts - self._segment_start_dts) + * packet.time_base + ) + >= MIN_SEGMENT_DURATION + ): + # Flush segment (also flushes the stub part segment) + self.flush(segment_duration, packet) + # Reinitialize + self.reset(packet.dts) + + # Mux the packet packet.stream = self._output_video_stream self._av_output.mux(packet) + self.check_flush_part(packet) + self._part_has_keyframe |= packet.is_keyframe + elif packet.stream == self._input_audio_stream: packet.stream = self._output_audio_stream self._av_output.mux(packet) - def flush(self, duration): + def check_flush_part(self, packet: av.Packet) -> None: + """Check for and mark a part segment boundary and record its duration.""" + byte_position = self._memory_file.tell() + if self._segment_last_write_pos == byte_position: + return + if self._segment is None: + # We have our first non-zero byte position. This means the init has just + # been written. Create a Segment and put it to the queue of each output. + self._segment = Segment( + sequence=self._sequence, + stream_id=self._stream_id, + init=self._memory_file.getvalue(), + ) + self._segment_last_write_pos = byte_position + # Fetch the latest StreamOutputs, which may have changed since the + # worker started. + for stream_output in self._outputs_callback().values(): + stream_output.put(self._segment) + else: # These are the ends of the part segments + self._segment.parts.append( + Part( + duration=float( + (packet.dts - self._part_start_dts) * packet.time_base + ), + has_keyframe=self._part_has_keyframe, + data=self._memory_file.getbuffer()[ + self._segment_last_write_pos : byte_position + ].tobytes(), + ) + ) + self._segment_last_write_pos = byte_position + self._part_start_dts = packet.dts + self._part_has_keyframe = False + + def flush(self, duration: Fraction, packet: av.Packet) -> None: """Create a segment from the buffered packets and write to output.""" self._av_output.close() - segment = Segment( - self._sequence, - *get_init_and_moof_data(self._memory_file.getbuffer()), - duration, - self._stream_id, + assert self._segment + self._segment.duration = float(duration) + # Also flush the part segment (need to close the output above before this) + self._segment.parts.append( + Part( + duration=float((packet.dts - self._part_start_dts) * packet.time_base), + has_keyframe=self._part_has_keyframe, + data=self._memory_file.getbuffer()[ + self._segment_last_write_pos : + ].tobytes(), + ) ) - self._memory_file.close() - for stream_output in self._outputs: - stream_output.put(segment) + self._memory_file.close() # We don't need the BytesIO object anymore - def discontinuity(self): + def discontinuity(self) -> None: """Mark the stream as having been restarted.""" # Preserving sequence and stream_id here keep the HLS playlist logic # simple to check for discontinuity at output time, and to determine # the discontinuity sequence number. self._stream_id += 1 - def close(self): + def close(self) -> None: """Close stream buffer.""" self._av_output.close() self._memory_file.close() -def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 +def stream_worker( # noqa: C901 + source: str, options: dict, segment_buffer: SegmentBuffer, quit_event: Event +) -> None: """Handle consuming streams.""" try: @@ -172,27 +234,27 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 audio_stream = None # Iterator for demuxing - container_packets = None + container_packets: Iterator[av.Packet] # The decoder timestamps of the latest packet in each stream we processed last_dts = {video_stream: float("-inf"), audio_stream: float("-inf")} # Keep track of consecutive packets without a dts to detect end of stream. missing_dts = 0 - # The video pts at the beginning of the segment - segment_start_pts = None + # The video dts at the beginning of the segment + segment_start_dts: int | None = None # Because of problems 1 and 2 below, we need to store the first few packets and replay them - initial_packets = deque() + initial_packets: deque[av.Packet] = deque() # Have to work around two problems with RTSP feeds in ffmpeg # 1 - first frame has bad pts/dts https://trac.ffmpeg.org/ticket/5018 # 2 - seeking can be problematic https://trac.ffmpeg.org/ticket/7815 - def peek_first_pts(): + def peek_first_dts() -> bool: """Initialize by peeking into the first few packets of the stream. Deal with problem #1 above (bad first packet pts/dts) by recalculating using pts/dts from second packet. - Also load the first video keyframe pts into segment_start_pts and check if the audio stream really exists. + Also load the first video keyframe dts into segment_start_dts and check if the audio stream really exists. """ - nonlocal segment_start_pts, audio_stream, container_packets + nonlocal segment_start_dts, audio_stream, container_packets missing_dts = 0 found_audio = False try: @@ -215,8 +277,8 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 elif packet.is_keyframe: # video_keyframe first_packet = packet initial_packets.append(packet) - # Get first_pts from subsequent frame to first keyframe - while segment_start_pts is None or ( + # Get first_dts from subsequent frame to first keyframe + while segment_start_dts is None or ( audio_stream and not found_audio and len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO @@ -244,11 +306,10 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 continue found_audio = True elif ( - segment_start_pts is None - ): # This is the second video frame to calculate first_pts from - segment_start_pts = packet.dts - packet.duration - first_packet.pts = segment_start_pts - first_packet.dts = segment_start_pts + segment_start_dts is None + ): # This is the second video frame to calculate first_dts from + segment_start_dts = packet.dts - packet.duration + first_packet.pts = first_packet.dts = segment_start_dts initial_packets.append(packet) if audio_stream and not found_audio: _LOGGER.warning( @@ -263,12 +324,13 @@ def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901 return False return True - if not peek_first_pts(): + if not peek_first_dts(): container.close() return segment_buffer.set_streams(video_stream, audio_stream) - segment_buffer.reset(segment_start_pts) + assert isinstance(segment_start_dts, int) + segment_buffer.reset(segment_start_dts) while not quit_event.is_set(): try: diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index ead2018b528..a73678d763f 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -9,13 +9,21 @@ nothing for the test to verify. The solution is the WorkerSync class that allows the tests to pause the worker thread before finalizing the stream so that it can inspect the output. """ +from __future__ import annotations + +import asyncio +from collections import deque import logging import threading from unittest.mock import patch +import async_timeout import pytest from homeassistant.components.stream import Stream +from homeassistant.components.stream.core import Segment + +TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout class WorkerSync: @@ -58,3 +66,57 @@ def stream_worker_sync(hass): autospec=True, ): yield sync + + +class SaveRecordWorkerSync: + """ + Test fixture to manage RecordOutput thread for recorder_save_worker. + + This is used to assert that the worker is started and stopped cleanly + to avoid thread leaks in tests. + """ + + def __init__(self): + """Initialize SaveRecordWorkerSync.""" + self._save_event = None + self._segments = None + self._save_thread = None + self.reset() + + def recorder_save_worker(self, file_out: str, segments: deque[Segment]): + """Mock method for patch.""" + logging.debug("recorder_save_worker thread started") + assert self._save_thread is None + self._segments = segments + self._save_thread = threading.current_thread() + self._save_event.set() + + async def get_segments(self): + """Return the recorded video segments.""" + with async_timeout.timeout(TEST_TIMEOUT): + await self._save_event.wait() + return self._segments + + async def join(self): + """Verify save worker was invoked and block on shutdown.""" + with async_timeout.timeout(TEST_TIMEOUT): + await self._save_event.wait() + self._save_thread.join(timeout=TEST_TIMEOUT) + assert not self._save_thread.is_alive() + + def reset(self): + """Reset callback state for reuse in tests.""" + self._save_thread = None + self._save_event = asyncio.Event() + + +@pytest.fixture() +def record_worker_sync(hass): + """Patch recorder_save_worker for clean thread shutdown for test.""" + sync = SaveRecordWorkerSync() + with patch( + "homeassistant.components.stream.recorder.recorder_save_worker", + side_effect=sync.recorder_save_worker, + autospec=True, + ): + yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index a31c686dcaf..37c499b6bd0 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -12,7 +12,7 @@ from homeassistant.components.stream.const import ( MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS, ) -from homeassistant.components.stream.core import Segment +from homeassistant.components.stream.core import Part, Segment from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -22,7 +22,7 @@ from tests.components.stream.common import generate_h264_video STREAM_SOURCE = "some-stream-source" INIT_BYTES = b"init" -MOOF_BYTES = b"some-bytes" +FAKE_PAYLOAD = b"fake-payload" SEGMENT_DURATION = 10 TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever @@ -70,23 +70,24 @@ def make_segment(segment, discontinuity=False): + "Z", ] ) - response.extend(["#EXTINF:10.0000,", f"./segment/{segment}.m4s"]), + response.extend([f"#EXTINF:{SEGMENT_DURATION:.3f},", f"./segment/{segment}.m4s"]) return "\n".join(response) -def make_playlist(sequence, discontinuity_sequence=0, segments=[]): +def make_playlist(sequence, segments, discontinuity_sequence=0): """Create a an hls playlist response for tests to assert on.""" response = [ "#EXTM3U", "#EXT-X-VERSION:6", - "#EXT-X-TARGETDURATION:10", + "#EXT-X-INDEPENDENT-SEGMENTS", '#EXT-X-MAP:URI="init.mp4"', + "#EXT-X-TARGETDURATION:10", f"#EXT-X-MEDIA-SEQUENCE:{sequence}", f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}", "#EXT-X-PROGRAM-DATE-TIME:" + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", - f"#EXT-X-START:TIME-OFFSET=-{1.5*SEGMENT_DURATION:.3f},PRECISE=YES", + f"#EXT-X-START:TIME-OFFSET=-{1.5*SEGMENT_DURATION:.3f}", ] response.extend(segments) response.append("") @@ -264,21 +265,26 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) - hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, SEGMENT_DURATION, start_time=FAKE_TIME)) + for i in range(2): + segment = Segment(sequence=i, duration=SEGMENT_DURATION, start_time=FAKE_TIME) + hls.put(segment) await hass.async_block_till_done() hls_client = await hls_stream(stream) resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 - assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)]) + assert await resp.text() == make_playlist( + sequence=0, segments=[make_segment(0), make_segment(1)] + ) - hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, SEGMENT_DURATION, start_time=FAKE_TIME)) + segment = Segment(sequence=2, duration=SEGMENT_DURATION, start_time=FAKE_TIME) + hls.put(segment) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 assert await resp.text() == make_playlist( - sequence=1, segments=[make_segment(1), make_segment(2)] + sequence=0, segments=[make_segment(0), make_segment(1), make_segment(2)] ) stream_worker_sync.resume() @@ -296,37 +302,40 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): hls_client = await hls_stream(stream) # Produce enough segments to overfill the output buffer by one - for sequence in range(1, MAX_SEGMENTS + 2): - hls.put( - Segment( - sequence, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - start_time=FAKE_TIME, - ) + for sequence in range(MAX_SEGMENTS + 1): + segment = Segment( + sequence=sequence, duration=SEGMENT_DURATION, start_time=FAKE_TIME ) + hls.put(segment) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist. - start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS + start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS segments = [] - for sequence in range(start, MAX_SEGMENTS + 2): + for sequence in range(start, MAX_SEGMENTS + 1): segments.append(make_segment(sequence)) - assert await resp.text() == make_playlist( - sequence=start, - segments=segments, - ) + assert await resp.text() == make_playlist(sequence=start, segments=segments) + + # Fetch the actual segments with a fake byte payload + for segment in hls.get_segments(): + segment.init = INIT_BYTES + segment.parts = [ + Part( + duration=SEGMENT_DURATION, + has_keyframe=True, + data=FAKE_PAYLOAD, + ) + ] # The segment that fell off the buffer is not accessible - segment_response = await hls_client.get("/segment/1.m4s") + segment_response = await hls_client.get("/segment/0.m4s") assert segment_response.status == 404 # However all segments in the buffer are accessible, even those that were not in the playlist. - for sequence in range(2, MAX_SEGMENTS + 2): + for sequence in range(1, MAX_SEGMENTS + 1): segment_response = await hls_client.get(f"/segment/{sequence}.m4s") assert segment_response.status == 200 @@ -342,36 +351,21 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) - hls.put( - Segment( - 1, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - stream_id=0, - start_time=FAKE_TIME, - ) + segment = Segment( + sequence=0, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME ) - hls.put( - Segment( - 2, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - stream_id=0, - start_time=FAKE_TIME, - ) + hls.put(segment) + segment = Segment( + sequence=1, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME ) - hls.put( - Segment( - 3, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - stream_id=1, - start_time=FAKE_TIME, - ) + hls.put(segment) + segment = Segment( + sequence=2, + stream_id=1, + duration=SEGMENT_DURATION, + start_time=FAKE_TIME, ) + hls.put(segment) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -379,11 +373,11 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 assert await resp.text() == make_playlist( - sequence=1, + sequence=0, segments=[ + make_segment(0), make_segment(1), - make_segment(2), - make_segment(3, discontinuity=True), + make_segment(2, discontinuity=True), ], ) @@ -401,29 +395,20 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy hls_client = await hls_stream(stream) - hls.put( - Segment( - 1, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - stream_id=0, - start_time=FAKE_TIME, - ) + segment = Segment( + sequence=0, stream_id=0, duration=SEGMENT_DURATION, start_time=FAKE_TIME ) + hls.put(segment) # Produce enough segments to overfill the output buffer by one - for sequence in range(1, MAX_SEGMENTS + 2): - hls.put( - Segment( - sequence, - INIT_BYTES, - MOOF_BYTES, - SEGMENT_DURATION, - stream_id=1, - start_time=FAKE_TIME, - ) + for sequence in range(MAX_SEGMENTS + 1): + segment = Segment( + sequence=sequence, + stream_id=1, + duration=SEGMENT_DURATION, + start_time=FAKE_TIME, ) + hls.put(segment) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") @@ -432,9 +417,9 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE # returned instead. - start = MAX_SEGMENTS + 2 - NUM_PLAYLIST_SEGMENTS + start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS segments = [] - for sequence in range(start, MAX_SEGMENTS + 2): + for sequence in range(start, MAX_SEGMENTS + 1): segments.append(make_segment(sequence)) assert await resp.text() == make_playlist( sequence=start, diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index d45dd0cbca7..07e1464f31a 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,23 +1,16 @@ """The tests for hls streams.""" -from __future__ import annotations - -import asyncio -from collections import deque from datetime import timedelta from io import BytesIO -import logging import os -import threading from unittest.mock import patch -import async_timeout import av import pytest from homeassistant.components.stream import create_stream from homeassistant.components.stream.const import HLS_PROVIDER, RECORDER_PROVIDER -from homeassistant.components.stream.core import Segment -from homeassistant.components.stream.fmp4utils import get_init_and_moof_data +from homeassistant.components.stream.core import Part, Segment +from homeassistant.components.stream.fmp4utils import find_box from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -26,63 +19,9 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video -TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever -class SaveRecordWorkerSync: - """ - Test fixture to manage RecordOutput thread for recorder_save_worker. - - This is used to assert that the worker is started and stopped cleanly - to avoid thread leaks in tests. - """ - - def __init__(self): - """Initialize SaveRecordWorkerSync.""" - self.reset() - self._segments = None - self._save_thread = None - - def recorder_save_worker(self, file_out: str, segments: deque[Segment]): - """Mock method for patch.""" - logging.debug("recorder_save_worker thread started") - assert self._save_thread is None - self._segments = segments - self._save_thread = threading.current_thread() - self._save_event.set() - - async def get_segments(self): - """Return the recorded video segments.""" - with async_timeout.timeout(TEST_TIMEOUT): - await self._save_event.wait() - return self._segments - - async def join(self): - """Verify save worker was invoked and block on shutdown.""" - with async_timeout.timeout(TEST_TIMEOUT): - await self._save_event.wait() - self._save_thread.join(timeout=TEST_TIMEOUT) - assert not self._save_thread.is_alive() - - def reset(self): - """Reset callback state for reuse in tests.""" - self._save_thread = None - self._save_event = asyncio.Event() - - -@pytest.fixture() -def record_worker_sync(hass): - """Patch recorder_save_worker for clean thread shutdown for test.""" - sync = SaveRecordWorkerSync() - with patch( - "homeassistant.components.stream.recorder.recorder_save_worker", - side_effect=sync.recorder_save_worker, - autospec=True, - ): - yield sync - - async def test_record_stream(hass, hass_client, record_worker_sync): """ Test record stream. @@ -179,6 +118,21 @@ async def test_record_path_not_allowed(hass, hass_client): await stream.async_record("/example/path") +def add_parts_to_segment(segment, source): + """Add relevant part data to segment for testing recorder.""" + moof_locs = list(find_box(source.getbuffer(), b"moof")) + [len(source.getbuffer())] + segment.init = source.getbuffer()[: moof_locs[0]].tobytes() + segment.parts = [ + Part( + duration=None, + has_keyframe=None, + http_range_start=None, + data=source.getbuffer()[moof_locs[i] : moof_locs[i + 1]], + ) + for i in range(1, len(moof_locs) - 1) + ] + + async def test_recorder_save(tmpdir): """Test recorder save.""" # Setup @@ -186,9 +140,10 @@ async def test_recorder_save(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker( - filename, [Segment(1, *get_init_and_moof_data(source.getbuffer()), 4)] - ) + segment = Segment(sequence=1) + add_parts_to_segment(segment, source) + segment.duration = 4 + recorder_save_worker(filename, [segment]) # Assert assert os.path.exists(filename) @@ -201,15 +156,13 @@ async def test_recorder_discontinuity(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - init, moof_data = get_init_and_moof_data(source.getbuffer()) - recorder_save_worker( - filename, - [ - Segment(1, init, moof_data, 4, 0), - Segment(2, init, moof_data, 4, 1), - ], - ) - + segment_1 = Segment(sequence=1, stream_id=0) + add_parts_to_segment(segment_1, source) + segment_1.duration = 4 + segment_2 = Segment(sequence=2, stream_id=1) + add_parts_to_segment(segment_2, source) + segment_2.duration = 4 + recorder_save_worker(filename, [segment_1, segment_2]) # Assert assert os.path.exists(filename) @@ -263,7 +216,9 @@ async def test_record_stream_audio( stream_worker_sync.resume() result = av.open( - BytesIO(last_segment.init + last_segment.moof_data), "r", format="mp4" + BytesIO(last_segment.init + last_segment.get_bytes_without_init()), + "r", + format="mp4", ) assert len(result.streams.audio) == expected_audio_streams diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index aa354ef41cb..74a4fa0e553 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -21,7 +21,7 @@ from unittest.mock import patch import av -from homeassistant.components.stream import Stream +from homeassistant.components.stream import Stream, create_stream from homeassistant.components.stream.const import ( HLS_PROVIDER, MAX_MISSING_DTS, @@ -29,6 +29,9 @@ from homeassistant.components.stream.const import ( TARGET_SEGMENT_DURATION, ) from homeassistant.components.stream.worker import SegmentBuffer, stream_worker +from homeassistant.setup import async_setup_component + +from tests.components.stream.common import generate_h264_video STREAM_SOURCE = "some-stream-source" # Formats here are arbitrary, not exercised by tests @@ -99,9 +102,9 @@ class PacketSequence: super().__init__(3) time_base = fractions.Fraction(1, VIDEO_FRAME_RATE) - dts = self.packet * PACKET_DURATION / time_base - pts = self.packet * PACKET_DURATION / time_base - duration = PACKET_DURATION / time_base + dts = int(self.packet * PACKET_DURATION / time_base) + pts = int(self.packet * PACKET_DURATION / time_base) + duration = int(PACKET_DURATION / time_base) stream = VIDEO_STREAM # Pretend we get 1 keyframe every second is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) @@ -177,6 +180,11 @@ class FakePyAvBuffer: """Capture the output segment for tests to inspect.""" self.segments.append(segment) + @property + def complete_segments(self): + """Return only the complete segments.""" + return [segment for segment in self.segments if segment.complete] + class MockPyAv: """Mocks out av.open.""" @@ -197,6 +205,19 @@ class MockPyAv: return self.container +class MockFlushPart: + """Class to hold a wrapper function for check_flush_part.""" + + # Wrap this method with a preceding write so the BytesIO pointer moves + check_flush_part = SegmentBuffer.check_flush_part + + @classmethod + def wrapped_check_flush_part(cls, segment_buffer, packet): + """Wrap check_flush_part to also advance the memory_file pointer.""" + segment_buffer._memory_file.write(b"0") + return cls.check_flush_part(segment_buffer, packet) + + async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream(hass, STREAM_SOURCE) @@ -209,6 +230,10 @@ async def async_decode_stream(hass, packets, py_av=None): with patch("av.open", new=py_av.open), patch( "homeassistant.components.stream.core.StreamOutput.put", side_effect=py_av.capture_buffer.capture_output_segment, + ), patch( + "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part", + side_effect=MockFlushPart.wrapped_check_flush_part, + autospec=True, ): segment_buffer = SegmentBuffer(stream.outputs) stream_worker(STREAM_SOURCE, {}, segment_buffer, threading.Event()) @@ -235,13 +260,16 @@ async def test_stream_worker_success(hass): hass, PacketSequence(TEST_SEQUENCE_LENGTH) ) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check number of segments. A segment is only formed when a packet from the next # segment arrives, hence the subtraction of one from the sequence length. - assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int( + (TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET + ) # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH assert len(decoded_stream.audio_packets) == 0 @@ -259,6 +287,7 @@ async def test_skip_out_of_order_packet(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # If skipped packet would have been the first packet of a segment, the previous @@ -273,12 +302,14 @@ async def test_skip_out_of_order_packet(hass): ) del segments[longer_segment_index] # Check number of segments - assert len(segments) == int((len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1) + assert len(complete_segments) == int( + (len(packets) - 1 - 1) * SEGMENTS_PER_PACKET - 1 + ) else: # Otherwise segment durations and number of segments are unaffected # Check number of segments - assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET) # Check remaining segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert len(decoded_stream.video_packets) == len(packets) - 1 assert len(decoded_stream.audio_packets) == 0 @@ -292,12 +323,15 @@ async def test_discard_old_packets(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check number of segments - assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int( + (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET + ) # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX assert len(decoded_stream.audio_packets) == 0 @@ -311,12 +345,15 @@ async def test_packet_overflow(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check number of segments - assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int( + (OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET + ) # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX assert len(decoded_stream.audio_packets) == 0 @@ -332,10 +369,11 @@ async def test_skip_initial_bad_packets(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert ( len(decoded_stream.video_packets) == num_packets @@ -344,7 +382,7 @@ async def test_skip_initial_bad_packets(hass): * KEYFRAME_INTERVAL ) # Check number of segments - assert len(segments) == int( + assert len(complete_segments) == int( (len(decoded_stream.video_packets) - 1) * SEGMENTS_PER_PACKET ) assert len(decoded_stream.audio_packets) == 0 @@ -381,13 +419,11 @@ async def test_skip_missing_dts(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations (not counting the last segment) - assert ( - sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))]) - >= len(segments) - 1 - ) + assert sum(segment.duration for segment in complete_segments) >= len(segments) - 1 assert len(decoded_stream.video_packets) == num_packets - num_bad_packets assert len(decoded_stream.audio_packets) == 0 @@ -403,8 +439,8 @@ async def test_too_many_bad_packets(hass): packets[i].dts = None decoded_stream = await async_decode_stream(hass, iter(packets)) - segments = decoded_stream.segments - assert len(segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) + complete_segments = decoded_stream.complete_segments + assert len(complete_segments) == int((bad_packet_start - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == bad_packet_start assert len(decoded_stream.audio_packets) == 0 @@ -431,8 +467,8 @@ async def test_audio_packets_not_found(hass): packets = PacketSequence(num_packets) # Contains only video packets decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - segments = decoded_stream.segments - assert len(segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) + complete_segments = decoded_stream.complete_segments + assert len(complete_segments) == int((num_packets - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets assert len(decoded_stream.audio_packets) == 0 @@ -444,8 +480,8 @@ async def test_adts_aac_audio(hass): num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 packets = list(PacketSequence(num_packets)) packets[1].stream = AUDIO_STREAM - packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE - packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) # The following is packet data is a sign of ADTS AAC packets[1][0] = 255 packets[1][1] = 241 @@ -462,17 +498,17 @@ async def test_audio_is_first_packet(hass): packets = list(PacketSequence(num_packets)) # Pair up an audio packet for each video packet packets[0].stream = AUDIO_STREAM - packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE - packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[0].dts = int(packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[0].pts = int(packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1 packets[2].stream = AUDIO_STREAM - packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE - packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # The audio packets are segmented with the video packets - assert len(segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int((num_packets - 2 - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets - 2 assert len(decoded_stream.audio_packets) == 1 @@ -484,13 +520,13 @@ async def test_audio_packets_found(hass): num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 packets = list(PacketSequence(num_packets)) packets[1].stream = AUDIO_STREAM - packets[1].dts = packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE - packets[1].pts = packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE + packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, iter(packets), py_av=py_av) - segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # The audio packet above is buffered with the video packet - assert len(segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int((num_packets - 1 - 1) * SEGMENTS_PER_PACKET) assert len(decoded_stream.video_packets) == num_packets - 1 assert len(decoded_stream.audio_packets) == 1 @@ -507,12 +543,15 @@ async def test_pts_out_of_order(hass): decoded_stream = await async_decode_stream(hass, iter(packets)) segments = decoded_stream.segments + complete_segments = decoded_stream.complete_segments # Check number of segments - assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET) + assert len(complete_segments) == int( + (TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET + ) # Check sequence numbers assert all(segments[i].sequence == i for i in range(len(segments))) # Check segment durations - assert all(s.duration == SEGMENT_DURATION for s in segments) + assert all(s.duration == SEGMENT_DURATION for s in complete_segments) assert len(decoded_stream.video_packets) == len(packets) assert len(decoded_stream.audio_packets) == 0 @@ -573,7 +612,11 @@ async def test_update_stream_source(hass): worker_wake.wait() return py_av.open(stream_source, args, kwargs) - with patch("av.open", new=blocking_open): + with patch("av.open", new=blocking_open), patch( + "homeassistant.components.stream.worker.SegmentBuffer.check_flush_part", + side_effect=MockFlushPart.wrapped_check_flush_part, + autospec=True, + ): stream.start() assert worker_open.wait(TIMEOUT) assert last_stream_source == STREAM_SOURCE @@ -604,3 +647,74 @@ async def test_worker_log(hass, caplog): await hass.async_block_till_done() assert "https://abcd:efgh@foo.bar" not in caplog.text assert "https://****:****@foo.bar" in caplog.text + + +async def test_durations(hass, record_worker_sync): + """Test that the duration metadata matches the media.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + source = generate_h264_video() + stream = create_stream(hass, source) + + # use record_worker_sync to grab output segments + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + + complete_segments = list(await record_worker_sync.get_segments())[:-1] + assert len(complete_segments) >= 1 + + # check that the Part duration metadata matches the durations in the media + running_metadata_duration = 0 + for segment in complete_segments: + for part in segment.parts: + av_part = av.open(io.BytesIO(segment.init + part.data)) + running_metadata_duration += part.duration + # av_part.duration will just return the largest dts in av_part. + # When we normalize by av.time_base this should equal the running duration + assert math.isclose( + running_metadata_duration, + av_part.duration / av.time_base, + abs_tol=1e-6, + ) + av_part.close() + # check that the Part durations are consistent with the Segment durations + for segment in complete_segments: + assert math.isclose( + sum(part.duration for part in segment.parts), segment.duration, abs_tol=1e-6 + ) + + await record_worker_sync.join() + + stream.stop() + + +async def test_has_keyframe(hass, record_worker_sync): + """Test that the has_keyframe metadata matches the media.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + source = generate_h264_video() + stream = create_stream(hass, source) + + # use record_worker_sync to grab output segments + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record("/example/path") + + # Our test video has keyframes every second. Use smaller parts so we have more + # part boundaries to better test keyframe logic. + with patch("homeassistant.components.stream.worker.TARGET_PART_DURATION", 0.25): + complete_segments = list(await record_worker_sync.get_segments())[:-1] + assert len(complete_segments) >= 1 + + # check that the Part has_keyframe metadata matches the keyframes in the media + for segment in complete_segments: + for part in segment.parts: + av_part = av.open(io.BytesIO(segment.init + part.data)) + media_has_keyframe = any( + packet.is_keyframe for packet in av_part.demux(av_part.streams.video[0]) + ) + av_part.close() + assert part.has_keyframe == media_has_keyframe + + await record_worker_sync.join() + + stream.stop() From 4300484ca0a39c5e7badaa12f2e7420ad6e56ce3 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 14 Jun 2021 00:20:02 +0200 Subject: [PATCH 336/750] Catch AsusWRT UnicodeDecodeError in get_nvram call (#51811) --- homeassistant/components/asuswrt/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 6f22ddbbd6e..4cea9148470 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -424,7 +424,7 @@ async def _get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: info = {} try: info = await api.async_get_nvram(info_type) - except OSError as exc: + except (OSError, UnicodeDecodeError) as exc: _LOGGER.warning("Error calling method async_get_nvram(%s): %s", info_type, exc) return info From d755952148bcfead68e8d9e46a72376a4bf8bd6a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 13 Jun 2021 17:24:46 -0500 Subject: [PATCH 337/750] Set playlist name on playing Sonos media (#51685) * Use playlist name as media_channel if available * Use proper playlist attribute --- homeassistant/components/sonos/media_player.py | 5 +++++ homeassistant/components/sonos/speaker.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 6d26363f83b..c44f7fbd4fb 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -366,6 +366,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Channel currently playing.""" return self.media.channel or None + @property + def media_playlist(self) -> str | None: + """Title of playlist currently playing.""" + return self.media.playlist_name + @property # type: ignore[misc] def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 2ddd7148478..59ed94adec6 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -14,7 +14,7 @@ import urllib.parse import async_timeout from pysonos.alarms import get_alarms from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo -from pysonos.data_structures import DidlAudioBroadcast +from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException from pysonos.music_library import MusicLibrary @@ -111,6 +111,7 @@ class SonosMedia: self.duration: float | None = None self.image_url: str | None = None self.queue_position: int | None = None + self.playlist_name: str | None = None self.source_name: str | None = None self.title: str | None = None self.uri: str | None = None @@ -125,6 +126,7 @@ class SonosMedia: self.channel = None self.duration = None self.image_url = None + self.playlist_name = None self.queue_position = None self.source_name = None self.title = None @@ -899,6 +901,9 @@ class SonosSpeaker: variables["enqueued_transport_uri"] or variables["current_track_uri"] ) music_source = self.soco.music_source_from_uri(track_uri) + if uri_meta_data := variables.get("enqueued_transport_uri_meta_data"): + if isinstance(uri_meta_data, DidlPlaylistContainer): + self.media.playlist_name = uri_meta_data.title else: self.media.play_mode = self.soco.play_mode music_source = self.soco.music_source From 75d6ffebc8fd257427efbc68c1f3656842838f1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Jun 2021 12:27:04 -1000 Subject: [PATCH 338/750] Improve error when HomeKit accessory underlying entity is missing (#51713) * Improve error when HomeKit accessory underlying entity is missing * docstring in test --- homeassistant/components/homekit/__init__.py | 11 +++- tests/components/homekit/test_homekit.py | 56 +++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 46fd3e5e522..c0cc5867799 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -599,7 +599,8 @@ class HomeKit: await self.hass.async_add_executor_job(self.setup, async_zc_instance) self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) await self.aid_storage.async_initialize() - await self._async_create_accessories() + if not await self._async_create_accessories(): + return self._async_register_bridge() _LOGGER.debug("Driver start for %s", self._name) await self.driver.async_start() @@ -666,6 +667,13 @@ class HomeKit: """Create the accessories.""" entity_states = await self.async_configure_accessories() if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: + if not entity_states: + _LOGGER.error( + "HomeKit %s cannot startup: entity not available: %s", + self._name, + self._filter.config, + ) + return False state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) @@ -677,6 +685,7 @@ class HomeKit: # No need to load/persist as we do it in setup self.driver.accessory = acc + return True async def async_stop(self, *args): """Stop the accessory driver.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index bd7af3b3596..5e9ea4fd4b6 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -54,7 +54,15 @@ from homeassistant.const import ( ) from homeassistant.core import State from homeassistant.helpers import device_registry -from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.helpers.entityfilter import ( + CONF_EXCLUDE_DOMAINS, + CONF_EXCLUDE_ENTITIES, + CONF_EXCLUDE_ENTITY_GLOBS, + CONF_INCLUDE_DOMAINS, + CONF_INCLUDE_ENTITIES, + CONF_INCLUDE_ENTITY_GLOBS, + convert_filter, +) from homeassistant.setup import async_setup_component from homeassistant.util import json as json_util @@ -65,6 +73,27 @@ from tests.common import MockConfigEntry, mock_device_registry, mock_registry IP_ADDRESS = "127.0.0.1" +def generate_filter( + include_domains, + include_entities, + exclude_domains, + exclude_entites, + include_globs=None, + exclude_globs=None, +): + """Generate an entity filter using the standard method.""" + return convert_filter( + { + CONF_INCLUDE_DOMAINS: include_domains, + CONF_INCLUDE_ENTITIES: include_entities, + CONF_EXCLUDE_DOMAINS: exclude_domains, + CONF_EXCLUDE_ENTITIES: exclude_entites, + CONF_INCLUDE_ENTITY_GLOBS: include_globs or [], + CONF_EXCLUDE_ENTITY_GLOBS: exclude_globs or [], + } + ) + + @pytest.fixture(autouse=True) def always_patch_driver(hk_driver): """Load the hk_driver fixture.""" @@ -1173,6 +1202,31 @@ async def test_homekit_start_in_accessory_mode( assert homekit.status == STATUS_RUNNING +async def test_homekit_start_in_accessory_mode_missing_entity( + hass, hk_driver, mock_zeroconf, device_reg, caplog +): + """Test HomeKit start method in accessory mode when entity is not available.""" + entry = await async_init_integration(hass) + + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) + + homekit.bridge = Mock() + homekit.bridge.accessories = [] + homekit.driver = hk_driver + homekit.driver.accessory = Accessory(hk_driver, "any") + + with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"): + await homekit.async_start() + + await hass.async_block_till_done() + mock_add_acc.assert_not_called() + assert homekit.status == STATUS_WAIT + + assert "entity not available" in caplog.text + + async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog): """Test we wait for the port to free before declaring unload success.""" await async_setup_component(hass, "persistent_notification", {}) From 1e00e48831ee7194ca025c576e93aebd9859e458 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 14 Jun 2021 00:09:44 +0000 Subject: [PATCH 339/750] [ci skip] Translation update --- .../components/ambee/translations/ca.json | 9 ++++++ .../components/ambee/translations/de.json | 26 +++++++++++++++++ .../components/ambee/translations/et.json | 9 ++++++ .../components/ambee/translations/it.json | 28 +++++++++++++++++++ .../components/ambee/translations/nl.json | 9 ++++++ .../components/ambee/translations/pl.json | 28 +++++++++++++++++++ .../ambee/translations/sensor.de.json | 10 +++++++ .../ambee/translations/sensor.it.json | 10 +++++++ .../ambee/translations/sensor.pl.json | 10 +++++++ .../components/blebox/translations/pl.json | 2 +- .../components/elgato/translations/pl.json | 2 +- .../components/ipp/translations/pl.json | 2 +- .../modern_forms/translations/it.json | 28 +++++++++++++++++++ .../modern_forms/translations/pl.json | 28 +++++++++++++++++++ .../components/roomba/translations/ca.json | 4 +-- .../components/roomba/translations/en.json | 4 +-- .../components/roomba/translations/et.json | 4 +-- .../components/roomba/translations/it.json | 4 +-- .../components/roomba/translations/pl.json | 4 +-- .../roomba/translations/zh-Hant.json | 4 +-- .../components/samsungtv/translations/de.json | 2 +- .../components/wled/translations/ca.json | 9 ++++++ .../components/wled/translations/it.json | 9 ++++++ .../components/wled/translations/nl.json | 9 ++++++ 24 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/ambee/translations/de.json create mode 100644 homeassistant/components/ambee/translations/it.json create mode 100644 homeassistant/components/ambee/translations/pl.json create mode 100644 homeassistant/components/ambee/translations/sensor.de.json create mode 100644 homeassistant/components/ambee/translations/sensor.it.json create mode 100644 homeassistant/components/ambee/translations/sensor.pl.json create mode 100644 homeassistant/components/modern_forms/translations/it.json create mode 100644 homeassistant/components/modern_forms/translations/pl.json diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json index a3611949083..ab3c9cb949e 100644 --- a/homeassistant/components/ambee/translations/ca.json +++ b/homeassistant/components/ambee/translations/ca.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API", + "description": "Torna a autenticar-te amb el compte d'Ambee." + } + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json new file mode 100644 index 00000000000..cbe6dc96303 --- /dev/null +++ b/homeassistant/components/ambee/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json index e6ec7654a64..085f13d6926 100644 --- a/homeassistant/components/ambee/translations/et.json +++ b/homeassistant/components/ambee/translations/et.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, "error": { "cannot_connect": "\u00dchendumine nurjus", "invalid_api_key": "Vale API v\u00f5ti" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti", + "description": "Taastuvasta Ambee konto" + } + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json new file mode 100644 index 00000000000..178e52a979f --- /dev/null +++ b/homeassistant/components/ambee/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API", + "description": "Riautenticati con il tuo account Ambee." + } + }, + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Configura Ambee per l'integrazione con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json index c8aaf5d9420..837e39a72d7 100644 --- a/homeassistant/components/ambee/translations/nl.json +++ b/homeassistant/components/ambee/translations/nl.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol" + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "Ongeldige API-sleutel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel", + "description": "Verifieer opnieuw met uw Ambee-account." + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json new file mode 100644 index 00000000000..d0b2225cc9a --- /dev/null +++ b/homeassistant/components/ambee/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API", + "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Ambee." + } + }, + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.de.json b/homeassistant/components/ambee/translations/sensor.de.json new file mode 100644 index 00000000000..c96a2c50eb7 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.de.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Hoch", + "low": "Niedrig", + "moderate": "M\u00e4\u00dfig", + "very high": "Sehr hoch" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.it.json b/homeassistant/components/ambee/translations/sensor.it.json new file mode 100644 index 00000000000..1c265a6ca53 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.it.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Alto", + "low": "Basso", + "moderate": "Moderato", + "very high": "Molto alto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pl.json b/homeassistant/components/ambee/translations/sensor.pl.json new file mode 100644 index 00000000000..64d04cced48 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.pl.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Wysoki", + "low": "Niski", + "moderate": "Umiarkowany", + "very high": "Bardzo wysoki" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json index 21540182505..0174380794e 100644 --- a/homeassistant/components/blebox/translations/pl.json +++ b/homeassistant/components/blebox/translations/pl.json @@ -16,7 +16,7 @@ "host": "Adres IP", "port": "Port" }, - "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistantem.", + "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 go z Home Assistantem.", "title": "Konfiguracja urz\u0105dzenia BleBox" } } diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json index 17b8522be35..94764903d10 100644 --- a/homeassistant/components/elgato/translations/pl.json +++ b/homeassistant/components/elgato/translations/pl.json @@ -14,7 +14,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Konfiguracja Elgato Light w celu integracji z Home Assistantem." + "description": "Skonfiguruj Elgato Light, aby zintegrowa\u0107 go z Home Assistantem." }, "zeroconf_confirm": { "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Light o numerze seryjnym `{serial_number}` do Home Assistanta?", diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index feba8470d7e..b44904095de 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -23,7 +23,7 @@ "ssl": "Certyfikat SSL", "verify_ssl": "Weryfikacja certyfikatu SSL" }, - "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistantem.", + "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol), aby zintegrowa\u0107 j\u0105 z Home Assistantem.", "title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119" }, "zeroconf_confirm": { diff --git a/homeassistant/components/modern_forms/translations/it.json b/homeassistant/components/modern_forms/translations/it.json new file mode 100644 index 00000000000..18f1d5f503a --- /dev/null +++ b/homeassistant/components/modern_forms/translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configura il tuo ventilatore Modern Forms per integrarlo con Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il ventilatore di Modern Forms chiamato `{name}` a Home Assistant?", + "title": "Rilevato il dispositivo ventilatore di Modern Forms" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/pl.json b/homeassistant/components/modern_forms/translations/pl.json new file mode 100644 index 00000000000..f68be10b7cd --- /dev/null +++ b/homeassistant/components/modern_forms/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Skonfiguruj Modern Forms, aby zintegrowa\u0107 go z Home Assistantem." + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 wentylator Modern Forms o nazwie {name} do Home Assistanta?", + "title": "Wykryto wentylator Modern Forms" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index d41b7d3833f..f237967b8a4 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -45,8 +45,8 @@ "host": "Amfitri\u00f3", "password": "Contrasenya" }, - "description": "Actualment la recuperaci\u00f3 de BLID i la contrasenya \u00e9s un proc\u00e9s manual. Segueix els passos de la documentaci\u00f3 a: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Connexi\u00f3 amb el dispositiu" + "description": "Selecciona un/a Roomba o Braava.", + "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu" } } }, diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 32853564e53..df95782f52f 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -45,8 +45,8 @@ "host": "Host", "password": "Password" }, - "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Connect to the device" + "description": "Select a Roomba or Braava.", + "title": "Automatically connect to the device" } } }, diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 0f992a57de6..7a8f33ebf57 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -45,8 +45,8 @@ "host": "", "password": "Salas\u00f5na" }, - "description": "Praegu on BLID ja parooli toomine k\u00e4sitsi protsess. J\u00e4rgi dokumentatsioonis toodud juhiseid aadressil: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "\u00dchendu seadmega" + "description": "Vali Roomba v\u00f5i Braava seade", + "title": "\u00dchenda seadmega automaatselt" } } }, diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index 0b2c9079ac9..d5909d5bcc5 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -45,8 +45,8 @@ "host": "Host", "password": "Password" }, - "description": "Attualmente il recupero del BLID e della password \u00e8 un processo manuale. Si prega di seguire i passi descritti nella documentazione all'indirizzo: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Connettersi al dispositivo" + "description": "Seleziona un Roomba o un Braava.", + "title": "Connetti automaticamente al dispositivo" } } }, diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index d4624a91906..118d5a8ece7 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -45,8 +45,8 @@ "host": "Nazwa hosta lub adres IP", "password": "Has\u0142o" }, - "description": "Obecnie pobieranie BLID i has\u0142a jest procesem r\u0119cznym. Prosz\u0119 post\u0119powa\u0107 zgodnie z instrukcjami zawartymi w dokumentacji pod adresem: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials.", - "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" + "description": "Wybierz Roomb\u0119 lub Braava", + "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" } } }, diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 81ba19a3a57..4a5891d896e 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -45,8 +45,8 @@ "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u76ee\u524d\u63a5\u6536 BLID \u8207\u5bc6\u78bc\u70ba\u624b\u52d5\u904e\u7a0b\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1ahttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" + "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002", + "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" } } }, diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index aa811944700..710443bc24f 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -7,7 +7,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "id_missing": "Dieses Samsung-Ger\u00e4t hat keine Seriennummer.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt.", - "reauth_successful": "Erneute Authentifizierung war erfolgreich", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json index bbc5b5232cf..2255a3cec0d 100644 --- a/homeassistant/components/wled/translations/ca.json +++ b/homeassistant/components/wled/translations/ca.json @@ -20,5 +20,14 @@ "title": "Dispositiu WLED descobert" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Mant\u00e9 el llum principal, fins i tot amb 1 segment LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index 874639638b7..efdc761960a 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -20,5 +20,14 @@ "title": "Dispositivo WLED rilevato" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Mantieni la luce principale, anche con 1 segmento LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index a06d49c8902..8423f2d3f48 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -20,5 +20,14 @@ "title": "Ontdekt WLED-apparaat" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Houd master light, zelfs met 1 LED-segment." + } + } + } } } \ No newline at end of file From c260fca24250ad5533690da7066945e840d5c5cd Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 14 Jun 2021 03:47:56 +0200 Subject: [PATCH 340/750] Bump pyialarm to 1.9.0 (#51804) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 08666129fd9..751faec56c7 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.8.1"], + "requirements": ["pyialarm==1.9.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index cd3d505a928..0a71353d9e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1469,7 +1469,7 @@ pyhomematic==0.1.73 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.8.1 +pyialarm==1.9.0 # homeassistant.components.icloud pyicloud==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99aafe4f418..1634564818a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -813,7 +813,7 @@ pyhiveapi==0.4.2 pyhomematic==0.1.73 # homeassistant.components.ialarm -pyialarm==1.8.1 +pyialarm==1.9.0 # homeassistant.components.icloud pyicloud==0.10.2 From cc622f46c5384eb04649242b8305160d1c70e808 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 13 Jun 2021 21:53:37 -0400 Subject: [PATCH 341/750] Bump up ZHA dependencies (#51765) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ec231ccc0e4..d1e79d1b67b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,13 +4,13 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.24.0", + "bellows==0.25.0", "pyserial==3.5", "pyserial-asyncio==0.5", "zha-quirks==0.0.57", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.33.0", + "zigpy==0.34.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index 0a71353d9e1..11896467ab0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -355,7 +355,7 @@ beautifulsoup4==4.9.3 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.24.0 +bellows==0.25.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 @@ -2438,7 +2438,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.33.0 +zigpy==0.34.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1634564818a..56d32dfdbde 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.24.0 +bellows==0.25.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.15 @@ -1326,7 +1326,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.33.0 +zigpy==0.34.0 # homeassistant.components.zwave_js zwave-js-server-python==0.26.1 From 0709aa7c8c361843d48d3f9170f76ff532d62c88 Mon Sep 17 00:00:00 2001 From: blawford Date: Mon, 14 Jun 2021 07:48:32 +0100 Subject: [PATCH 342/750] Pass metadata when casting an app (#51148) * Pass through metadata when casting an app * Remove passing kwargs to quick_play Add metadata to the app_data dict. * Include pass-through of metadata * Bump pychromecast to 9.2.0 * Add changes to test to verify metadata * Fix order of imports --- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_media_player.py | 20 +++++++++++++++---- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index c104ff7a12e..78f7bcf485c 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.1.2"], + "requirements": ["pychromecast==9.2.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 969e690fcc2..07e97dd1a7e 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -482,10 +482,15 @@ class CastDevice(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Play media from a URL.""" + extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) + metadata = extra.get("metadata") + # We do not want this to be forwarded to a group if media_type == CAST_DOMAIN: try: app_data = json.loads(media_id) + if metadata is not None: + app_data["metadata"] = extra.get("metadata") except json.JSONDecodeError: _LOGGER.error("Invalid JSON in media_content_id") raise diff --git a/requirements_all.txt b/requirements_all.txt index 11896467ab0..cec3e65f1f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1330,7 +1330,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.1.2 +pychromecast==9.2.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56d32dfdbde..adf047b3a4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -740,7 +740,7 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.1.2 +pychromecast==9.2.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 959d53184a4..3bb2b895c1a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -10,7 +10,7 @@ import attr import pychromecast import pytest -from homeassistant.components import tts +from homeassistant.components import media_player, tts from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.components.media_player.const import ( @@ -27,7 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -702,8 +702,20 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock): chromecast.start_app.assert_called_once_with("abc123") # Play_media - cast with app name (quick play) - await common.async_play_media(hass, "cast", '{"app_name": "youtube"}', entity_id) - quick_play_mock.assert_called_once_with(ANY, "youtube", {}) + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: entity_id, + media_player.ATTR_MEDIA_CONTENT_TYPE: "cast", + media_player.ATTR_MEDIA_CONTENT_ID: '{"app_name":"youtube"}', + media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, + }, + blocking=True, + ) + quick_play_mock.assert_called_once_with( + ANY, "youtube", {"metadata": {"metadatatype": 3}} + ) async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): From 7e1fec8ee4e8a99bac335a76996dc7ee5eb161e0 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Mon, 14 Jun 2021 08:58:42 +0200 Subject: [PATCH 343/750] Rewrite of Yamaha musiccast integration (#51561) * Initial commit for new musiccast integration * Add zone support * Get/set volume level * Remove volume step * Create custom MusicCastData type * Create MusicCastDevice * Fix await * Add power and mute control * Implement all basic media_player parts * Support input switching * Add duration/position support * Add advanced tuner functions * Basic media browser * Add layer in media browser to see all available list_infos * Added join/unjoin services and group informations. Known issue: You can not link zone 2 to main at the moment (WIP) * Many fixes to make multiple zones and grouping work. Next step: implement error handling and remove debugging information * WIP: Added Multizone Support and allows clients to directly jump from one group to another. Known issue: If a server tries to join a group as client, he has to close his group first. Sometimes the device that was a server previously jumps out of the group directly after joining. * Updated group management to make it wait for the updated group information before performing the next actions - Timeouts after 1 second, then polls the distribution data. If the data are still not updated, there will be one retry before an Exception is thrown. Extended the state attributes for clients to make them return group details from their servers (leads to inactive group management buttons for the client). Added documentation and restructured the code. * Make the service handle function name for group specific service calls unique * Added service descriptions for set_sleep_timer, set_alarm, recall_netusb_preset, store_netusb_preset * Added data entries for alarm specific values and a netusb preset list. Implemented fetching function for clock and netusb presets. * Registered and implemented services for set_sleep_timer, set_alarm, recall_netusb_preset, store_netusb_preset. The set_alarm service works with a special mediaplayer alarm lovelace card, I am currently working on. The NetUSB Presets are also available using the media browser. Maybe we could also add the Tuner presets in the future for both setting up the alarm and recalling them via service and media browser. * Removed some debug prints * Moved MusicCast Integration to the aiomusiccast library. This library supports media browsers with multiple pages. Added ssdp support for the discovery * Minor fix in the group management and tidied up a bit * Updated manifest of yamaha musiccast * Update library * Minor fix in the media browser. get_distribution_num does not have to be async, so it has been changed. Adjusted the client join function to turn on the client before joining a group - the musiccast app does so, so hopefully this fixes the rare errors when adding a turned off client to a group. Some reformating and by hooks fixed most of the requirements of the hooks. Known exception from this: mypy throws an error for line 116. * Removed some old out commented code. Fixed some error handling, when the user enters a non reachable or non yamaha host in the manual setup. Fixed linting/styling errors. Implemented tests to bring the coverage for the config flow to 100%. * Fixed linting/styling errors. Return a DeviceInfo object instead of a dict. * Fixed linting/styling errors. Added a new error type to the translations. * In the yamaha API the system_id is equal to the serial number in the DLNA description. Due to that it was possible to configure a device twice, because the serial number from the yamaha API was different. This issue was fixed. * Updated tests and added a test for adding a device manually, which is already present in the system * Remove print statements * Fix sleep timer service call * Fix yamllint error * Shrink PR down to just new library + config flow with discovery * Add __init__.py to .coveragerc * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof * Implement suggestions from code review * Improve identifiers and connections, remove event loop parameter * Add coordinator back * Better exception handling * Fix unique id in ssdp step * Remove abc.ABC from MusicCasteDeviceEntity Co-authored-by: Martin Hjelmare * Update homeassistant/components/yamaha_musiccast/config_flow.py Co-authored-by: Martin Hjelmare * Replace the repeat mode mapping from mc to ha by a generic solution Co-authored-by: Martin Hjelmare * add coordinator to the super call of the mediaplayer Co-authored-by: Martin Hjelmare * add the coordinator to the init function of the MusicCastEntity Co-authored-by: Martin Hjelmare * Pass the coordinator from the MusicCastEntity init function to the CoordinatorEntity init function Co-authored-by: Martin Hjelmare * merged _handle_config_flow into async_step_user * reformated the exception handling of the user step. In the case that the device already exists, the AbortFlow will be raised. * Removed model from the config entry. It was neither set nor used anymore. * Fixed the test for the config flow. * Use async_write_ha_state instead of schedule_update_ha_state. * Add default value for the system ID gotten in the user step Co-authored-by: Martin Hjelmare * Update tests/components/yamaha_musiccast/test_config_flow.py Co-authored-by: Martin Hjelmare * Added a fixture to avoid IO in the test_user_input_device_not_found test * Use absolute imprt to import data_entry_flow. * Use local vars for host and serial_number in async_step_user. * Remove ip_address and zone_id properties. * Use device id for the unique ID of an entity instead of the macs * Removed entry_id from the MusicCastEntity init function. * Updated strings and English translation. * don't set the coordinator in the mediaplayer init. * Implemented legacy configuration.yaml support for existing configurations. * Added tests for the newly added config flow step. * Use device_id as identifier * Fix an accidentally relative import * Fix pylint warnings * use logger.error instead of logger.exception in the import step. Co-authored-by: Martin Hjelmare * Use CONF_HOST instead of 'host' * Only support the import from configuration.yaml if no config entries are setup for musiccast. If there are already config entries in HA and none of them is a representation of a config given in configuration.yaml (e.g. config added after the first import), an error will be logged. * Update homeassistant/components/yamaha_musiccast/media_player.py Co-authored-by: Martin Hjelmare * Readded PLATFORM_SCHEMA for configuration.yaml * Raise an exception for all services, which are only supported for specific sources. * Bump aiomusiccast to 0.6 to support asyncio sockets Co-authored-by: Michael Harbarth Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/yamaha_musiccast/__init__.py | 135 ++++- .../yamaha_musiccast/config_flow.py | 130 +++++ .../components/yamaha_musiccast/const.py | 31 + .../components/yamaha_musiccast/manifest.json | 21 +- .../yamaha_musiccast/media_player.py | 543 +++++++++++------- .../components/yamaha_musiccast/strings.json | 23 + .../yamaha_musiccast/translations/en.json | 23 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + tests/components/yamaha_musiccast/__init__.py | 1 + .../yamaha_musiccast/test_config_flow.py | 287 +++++++++ 15 files changed, 990 insertions(+), 222 deletions(-) create mode 100644 homeassistant/components/yamaha_musiccast/config_flow.py create mode 100644 homeassistant/components/yamaha_musiccast/const.py create mode 100644 homeassistant/components/yamaha_musiccast/strings.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/en.json create mode 100644 tests/components/yamaha_musiccast/__init__.py create mode 100644 tests/components/yamaha_musiccast/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index c407c2ab373..4b0a1f8c4a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1212,6 +1212,7 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* homeassistant/components/yale_smart_alarm/alarm_control_panel.py + homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yandex_transport/* homeassistant/components/yeelightsunflower/light.py diff --git a/CODEOWNERS b/CODEOWNERS index 0a5c9503dba..158b375163f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -568,7 +568,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yale_smart_alarm/* @gjohansson-ST -homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelightsunflower/* @lindsaymarkward diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index bf270b508d9..d3749da318c 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -1 +1,134 @@ -"""The yamaha_musiccast component.""" +"""The MusicCast integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aiomusiccast import MusicCastConnectionException +from aiomusiccast.musiccast_device import MusicCastData, MusicCastDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import BRAND, DOMAIN + +PLATFORMS = ["media_player"] + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=60) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up MusicCast from a config entry.""" + + client = MusicCastDevice(entry.data[CONF_HOST], async_get_clientsession(hass)) + coordinator = MusicCastDataUpdateCoordinator(hass, client=client) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]): + """Class to manage fetching data from the API.""" + + def __init__(self, hass: HomeAssistant, client: MusicCastDevice) -> None: + """Initialize.""" + self.musiccast = client + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self) -> MusicCastData: + """Update data via library.""" + try: + await self.musiccast.fetch() + except MusicCastConnectionException as exception: + raise UpdateFailed() from exception + return self.musiccast.data + + +class MusicCastEntity(CoordinatorEntity): + """Defines a base MusicCast entity.""" + + coordinator: MusicCastDataUpdateCoordinator + + def __init__( + self, + *, + name: str, + icon: str, + coordinator: MusicCastDataUpdateCoordinator, + enabled_default: bool = True, + ) -> None: + """Initialize the MusicCast entity.""" + super().__init__(coordinator) + self._enabled_default = enabled_default + self._icon = icon + self._name = name + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + +class MusicCastDeviceEntity(MusicCastEntity): + """Defines a MusicCast device entity.""" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this MusicCast device.""" + return DeviceInfo( + connections={ + (CONNECTION_NETWORK_MAC, format_mac(mac)) + for mac in self.coordinator.data.mac_addresses.values() + }, + identifiers={ + ( + DOMAIN, + self.coordinator.data.device_id, + ) + }, + name=self.coordinator.data.network_name, + manufacturer=BRAND, + model=self.coordinator.data.model_name, + sw_version=self.coordinator.data.system_version, + ) diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py new file mode 100644 index 00000000000..06bb212e639 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for MusicCast.""" +from __future__ import annotations + +import logging +from urllib.parse import urlparse + +from aiohttp import ClientConnectorError +from aiomusiccast import MusicCastConnectionException, MusicCastDevice +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a MusicCast config flow.""" + + VERSION = 1 + + serial_number: str | None = None + host: str + + async def async_step_user( + self, user_input: ConfigType | None = None + ) -> data_entry_flow.FlowResult: + """Handle a flow initiated by the user.""" + # Request user input, unless we are preparing discovery flow + if user_input is None: + return self._show_setup_form() + + host = user_input[CONF_HOST] + serial_number = None + + errors = {} + # Check if device is a MusicCast device + + try: + info = await MusicCastDevice.get_device_info( + host, async_get_clientsession(self.hass) + ) + except (MusicCastConnectionException, ClientConnectorError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + serial_number = info.get("system_id") + if serial_number is None: + errors["base"] = "no_musiccast_device" + + if not errors: + await self.async_set_unique_id(serial_number, raise_on_progress=False) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=host, + data={ + CONF_HOST: host, + "serial": serial_number, + }, + ) + + return self._show_setup_form(errors) + + def _show_setup_form( + self, errors: dict | None = None + ) -> data_entry_flow.FlowResult: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) + + async def async_step_ssdp(self, discovery_info) -> data_entry_flow.FlowResult: + """Handle ssdp discoveries.""" + if not await MusicCastDevice.check_yamaha_ssdp( + discovery_info[ssdp.ATTR_SSDP_LOCATION], async_get_clientsession(self.hass) + ): + return self.async_abort(reason="yxc_control_url_missing") + + self.serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL] + self.host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname + await self.async_set_unique_id(self.serial_number) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + self.context.update( + { + "title_placeholders": { + "name": discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host) + } + } + ) + + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None) -> data_entry_flow.FlowResult: + """Allow the user to confirm adding the device.""" + if user_input is not None: + return self.async_create_entry( + title=self.host, + data={ + CONF_HOST: self.host, + "serial": self.serial_number, + }, + ) + + return self.async_show_form(step_id="confirm") + + async def async_step_import(self, import_data: dict) -> data_entry_flow.FlowResult: + """Import data from configuration.yaml into the config flow.""" + res = await self.async_step_user(import_data) + if res["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + _LOGGER.info( + "Successfully imported %s from configuration.yaml", + import_data.get(CONF_HOST), + ) + elif res["type"] == data_entry_flow.RESULT_TYPE_FORM: + _LOGGER.error( + "Could not import %s from configuration.yaml", + import_data.get(CONF_HOST), + ) + return res diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py new file mode 100644 index 00000000000..422f93e1562 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -0,0 +1,31 @@ +"""Constants for the MusicCast integration.""" +from homeassistant.components.media_player.const import ( + REPEAT_MODE_ALL, + REPEAT_MODE_OFF, + REPEAT_MODE_ONE, +) + +DOMAIN = "yamaha_musiccast" + +BRAND = "Yamaha Corporation" + +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_PLAYLIST = "playlist" +ATTR_PRESET = "preset" +ATTR_SOFTWARE_VERSION = "sw_version" + +DEFAULT_ZONE = "main" +HA_REPEAT_MODE_TO_MC_MAPPING = { + REPEAT_MODE_OFF: "off", + REPEAT_MODE_ONE: "one", + REPEAT_MODE_ALL: "all", +} + +INTERVAL_SECONDS = "interval_seconds" + +MC_REPEAT_MODE_TO_HA_MAPPING = { + val: key for key, val in HA_REPEAT_MODE_TO_MC_MAPPING.items() +} diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 4a0294f444c..1ff4e2efdf4 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -1,8 +1,19 @@ { "domain": "yamaha_musiccast", - "name": "Yamaha MusicCast", + "name": "MusicCast", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["pymusiccast==0.1.6"], - "codeowners": ["@jalmeroth"], - "iot_class": "local_polling" -} + "requirements": [ + "aiomusiccast==0.6" + ], + "ssdp": [ + { + "manufacturer": "Yamaha Corporation" + } + ], + "iot_class": "local_push", + "codeowners": [ + "@vigonotion", + "@micha91" + ] +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index b21f6d3a3f4..aab2c8df3d2 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,216 +1,402 @@ -"""Support for Yamaha MusicCast Receivers.""" -import logging -import socket +"""Implementation of the musiccast media player.""" +from __future__ import annotations + +import logging -import pymusiccast import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, + REPEAT_MODE_OFF, + SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, STATE_IDLE, - STATE_ON, + STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, ) +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType, HomeAssistantType + +from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity +from .const import ( + DEFAULT_ZONE, + DOMAIN, + HA_REPEAT_MODE_TO_MC_MAPPING, + INTERVAL_SECONDS, + MC_REPEAT_MODE_TO_HA_MAPPING, +) _LOGGER = logging.getLogger(__name__) -SUPPORTED_FEATURES = ( - SUPPORT_PLAY - | SUPPORT_PAUSE - | SUPPORT_STOP - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF +MUSIC_PLAYER_SUPPORT = ( + SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_PLAY + | SUPPORT_SHUFFLE_SET + | SUPPORT_REPEAT_SET + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP ) -KNOWN_HOSTS_KEY = "data_yamaha_musiccast" -INTERVAL_SECONDS = "interval_seconds" - -DEFAULT_PORT = 5005 -DEFAULT_INTERVAL = 480 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(INTERVAL_SECONDS, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional(CONF_PORT, default=5000): cv.port, + vol.Optional(INTERVAL_SECONDS, default=0): cv.positive_int, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yamaha MusicCast platform.""" +async def async_setup_platform( + hass: HomeAssistantType, + config, + async_add_devices: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Import legacy configurations.""" - known_hosts = hass.data.get(KNOWN_HOSTS_KEY) - if known_hosts is None: - known_hosts = hass.data[KNOWN_HOSTS_KEY] = [] - _LOGGER.debug("known_hosts: %s", known_hosts) - - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - interval = config.get(INTERVAL_SECONDS) - - # Get IP of host to prevent duplicates - try: - ipaddr = socket.gethostbyname(host) - except (OSError) as error: - _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) - return - - if [item for item in known_hosts if item[0] == ipaddr]: - _LOGGER.warning("Host %s:%d already registered", host, port) - return - - if [item for item in known_hosts if item[1] == port]: - _LOGGER.warning("Port %s:%d already registered", host, port) - return - - reg_host = (ipaddr, port) - known_hosts.append(reg_host) - - try: - receiver = pymusiccast.McDevice(ipaddr, udp_port=port, mc_interval=interval) - except pymusiccast.exceptions.YMCInitError as err: - _LOGGER.error(err) - receiver = None - - if receiver: - for zone in receiver.zones: - _LOGGER.debug("Receiver: %s / Port: %d / Zone: %s", receiver, port, zone) - add_entities([YamahaDevice(receiver, receiver.zones[zone])], True) + if hass.config_entries.async_entries(DOMAIN) and config[CONF_HOST] not in [ + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ]: + _LOGGER.error( + "Configuration in configuration.yaml is not supported anymore. " + "Please add this device using the config flow: %s", + config[CONF_HOST], + ) else: - known_hosts.remove(reg_host) + _LOGGER.warning( + "Configuration in configuration.yaml is deprecated. Use the config flow instead" + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) -class YamahaDevice(MediaPlayerEntity): - """Representation of a Yamaha MusicCast device.""" +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MusicCast sensor based on a config entry.""" + coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - def __init__(self, recv, zone): - """Initialize the Yamaha MusicCast device.""" - self._recv = recv - self._name = recv.name - self._source = None - self._source_list = [] - self._zone = zone - self.mute = False - self.media_status = None - self.media_status_received = None - self.power = STATE_UNKNOWN - self.status = STATE_UNKNOWN - self.volume = 0 - self.volume_max = 0 - self._recv.set_yamaha_device(self) - self._zone.set_yamaha_device(self) + name = coordinator.data.network_name + + media_players: list[Entity] = [] + + for zone in coordinator.data.zones: + zone_name = name if zone == DEFAULT_ZONE else f"{name} {zone}" + + media_players.append( + MusicCastMediaPlayer(zone, zone_name, entry.entry_id, coordinator) + ) + + async_add_entities(media_players) + + +class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): + """The musiccast media player.""" + + def __init__(self, zone_id, name, entry_id, coordinator): + """Initialize the musiccast device.""" + self._player_state = STATE_PLAYING + self._volume_muted = False + self._shuffle = False + self._zone_id = zone_id + + super().__init__( + name=name, + icon="mdi:speaker", + coordinator=coordinator, + ) + + self._volume_min = self.coordinator.data.zones[self._zone_id].min_volume + self._volume_max = self.coordinator.data.zones[self._zone_id].max_volume + + self._cur_track = 0 + self._repeat = REPEAT_MODE_OFF + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + await super().async_added_to_hass() + # Sensors should also register callbacks to HA when their state changes + self.coordinator.musiccast.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + await super().async_will_remove_from_hass() + # The opposite of async_added_to_hass. Remove any registered call backs here. + self.coordinator.musiccast.remove_callback(self.async_write_ha_state) @property - def name(self): - """Return the name of the device.""" - return f"{self._name} ({self._zone.zone_id})" + def should_poll(self): + """Push an update after each command.""" + return False + + @property + def _is_netusb(self): + return ( + self.coordinator.data.netusb_input + == self.coordinator.data.zones[self._zone_id].input + ) + + @property + def _is_tuner(self): + return self.coordinator.data.zones[self._zone_id].input == "tuner" @property def state(self): - """Return the state of the device.""" - if self.power == STATE_ON and self.status != STATE_UNKNOWN: - return self.status - return self.power - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self.mute + """Return the state of the player.""" + if self.coordinator.data.zones[self._zone_id].power == "on": + if self._is_netusb and self.coordinator.data.netusb_playback == "pause": + return STATE_PAUSED + if self._is_netusb and self.coordinator.data.netusb_playback == "stop": + return STATE_IDLE + return STATE_PLAYING + return STATE_OFF @property def volume_level(self): - """Volume level of the media player (0..1).""" - return self.volume + """Return the volume level of the media player (0..1).""" + volume = self.coordinator.data.zones[self._zone_id].current_volume + return (volume - self._volume_min) / (self._volume_max - self._volume_min) + + @property + def is_volume_muted(self): + """Return boolean if volume is currently muted.""" + return self.coordinator.data.zones[self._zone_id].mute + + @property + def shuffle(self): + """Boolean if shuffling is enabled.""" + return ( + self.coordinator.data.netusb_shuffle == "on" if self._is_netusb else False + ) + + @property + def sound_mode(self): + """Return the current sound mode.""" + return self.coordinator.data.zones[self._zone_id].sound_program + + @property + def sound_mode_list(self): + """Return a list of available sound modes.""" + return self.coordinator.data.zones[self._zone_id].sound_program_list + + @property + def zone(self): + """Return the zone of the media player.""" + return self._zone_id + + @property + def unique_id(self) -> str: + """Return the unique ID for this media_player.""" + return f"{self.coordinator.data.device_id}_{self._zone_id}" + + async def async_turn_on(self): + """Turn the media player on.""" + await self.coordinator.musiccast.turn_on(self._zone_id) + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn the media player off.""" + await self.coordinator.musiccast.turn_off(self._zone_id) + self.async_write_ha_state() + + async def async_mute_volume(self, mute): + """Mute the volume.""" + + await self.coordinator.musiccast.mute_volume(self._zone_id, mute) + self.async_write_ha_state() + + async def async_set_volume_level(self, volume): + """Set the volume level, range 0..1.""" + await self.coordinator.musiccast.set_volume_level(self._zone_id, volume) + self.async_write_ha_state() + + async def async_media_play(self): + """Send play command.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_play() + else: + raise HomeAssistantError( + "Service play is not supported for non NetUSB sources." + ) + + async def async_media_pause(self): + """Send pause command.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_pause() + else: + raise HomeAssistantError( + "Service pause is not supported for non NetUSB sources." + ) + + async def async_media_stop(self): + """Send stop command.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_pause() + else: + raise HomeAssistantError( + "Service stop is not supported for non NetUSB sources." + ) + + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_shuffle(shuffle) + else: + raise HomeAssistantError( + "Service shuffle is not supported for non NetUSB sources." + ) + + async def async_select_sound_mode(self, sound_mode): + """Select sound mode.""" + await self.coordinator.musiccast.select_sound_mode(self._zone_id, sound_mode) + + @property + def media_image_url(self): + """Return the image url of current playing media.""" + return self.coordinator.musiccast.media_image_url if self._is_netusb else None + + @property + def media_title(self): + """Return the title of current playing media.""" + if self._is_netusb: + return self.coordinator.data.netusb_track + if self._is_tuner: + return self.coordinator.musiccast.tuner_media_title + + return None + + @property + def media_artist(self): + """Return the artist of current playing media (Music track only).""" + if self._is_netusb: + return self.coordinator.data.netusb_artist + if self._is_tuner: + return self.coordinator.musiccast.tuner_media_artist + + return None + + @property + def media_album_name(self): + """Return the album of current playing media (Music track only).""" + return self.coordinator.data.netusb_album if self._is_netusb else None + + @property + def repeat(self): + """Return current repeat mode.""" + return ( + MC_REPEAT_MODE_TO_HA_MAPPING.get(self.coordinator.data.netusb_repeat) + if self._is_netusb + else REPEAT_MODE_OFF + ) @property def supported_features(self): - """Flag of features that are supported.""" - return SUPPORTED_FEATURES + """Flag media player features that are supported.""" + return MUSIC_PLAYER_SUPPORT + + async def async_media_previous_track(self): + """Send previous track command.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_previous_track() + elif self._is_tuner: + await self.coordinator.musiccast.tuner_previous_station() + else: + raise HomeAssistantError( + "Service previous track is not supported for non NetUSB or Tuner sources." + ) + + async def async_media_next_track(self): + """Send next track command.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_next_track() + elif self._is_tuner: + await self.coordinator.musiccast.tuner_next_station() + else: + raise HomeAssistantError( + "Service next track is not supported for non NetUSB or Tuner sources." + ) + + def clear_playlist(self): + """Clear players playlist.""" + self._cur_track = 0 + self._player_state = STATE_OFF + self.async_write_ha_state() + + async def async_set_repeat(self, repeat): + """Enable/disable repeat mode.""" + if self._is_netusb: + await self.coordinator.musiccast.netusb_repeat( + HA_REPEAT_MODE_TO_MC_MAPPING.get(repeat, "off") + ) + else: + raise HomeAssistantError( + "Service set repeat is not supported for non NetUSB sources." + ) + + async def async_select_source(self, source): + """Select input source.""" + await self.coordinator.musiccast.select_source(self._zone_id, source) @property def source(self): - """Return the current input source.""" - return self._source + """Name of the current input source.""" + return self.coordinator.data.zones[self._zone_id].input @property def source_list(self): """List of available input sources.""" - return self._source_list - - @source_list.setter - def source_list(self, value): - """Set source_list attribute.""" - self._source_list = value - - @property - def media_content_type(self): - """Return the media content type.""" - return MEDIA_TYPE_MUSIC + return self.coordinator.data.zones[self._zone_id].input_list @property def media_duration(self): """Duration of current playing media in seconds.""" - return self.media_status.media_duration if self.media_status else None + if self._is_netusb: + return self.coordinator.data.netusb_total_time - @property - def media_image_url(self): - """Image url of current playing media.""" - return self.media_status.media_image_url if self.media_status else None - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self.media_status.media_artist if self.media_status else None - - @property - def media_album(self): - """Album of current playing media, music track only.""" - return self.media_status.media_album if self.media_status else None - - @property - def media_track(self): - """Track number of current playing media, music track only.""" - return self.media_status.media_track if self.media_status else None - - @property - def media_title(self): - """Title of current playing media.""" - return self.media_status.media_title if self.media_status else None + return None @property def media_position(self): """Position of current playing media in seconds.""" - if self.media_status and self.state in [ - STATE_PLAYING, - STATE_PAUSED, - STATE_IDLE, - ]: - return self.media_status.media_position + if self._is_netusb: + return self.coordinator.data.netusb_play_time + + return None @property def media_position_updated_at(self): @@ -218,74 +404,7 @@ class YamahaDevice(MediaPlayerEntity): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_received if self.media_status else None + if self._is_netusb: + return self.coordinator.data.netusb_play_time_updated - def update(self): - """Get the latest details from the device.""" - _LOGGER.debug("update: %s", self.entity_id) - self._recv.update_status() - self._zone.update_status() - - def update_hass(self): - """Push updates to Home Assistant.""" - if self.entity_id: - _LOGGER.debug("update_hass: pushing updates") - self.schedule_update_ha_state() - return True - - def turn_on(self): - """Turn on specified media player or all.""" - _LOGGER.debug("Turn device: on") - self._zone.set_power(True) - - def turn_off(self): - """Turn off specified media player or all.""" - _LOGGER.debug("Turn device: off") - self._zone.set_power(False) - - def media_play(self): - """Send the media player the command for play/pause.""" - _LOGGER.debug("Play") - self._recv.set_playback("play") - - def media_pause(self): - """Send the media player the command for pause.""" - _LOGGER.debug("Pause") - self._recv.set_playback("pause") - - def media_stop(self): - """Send the media player the stop command.""" - _LOGGER.debug("Stop") - self._recv.set_playback("stop") - - def media_previous_track(self): - """Send the media player the command for prev track.""" - _LOGGER.debug("Previous") - self._recv.set_playback("previous") - - def media_next_track(self): - """Send the media player the command for next track.""" - _LOGGER.debug("Next") - self._recv.set_playback("next") - - def mute_volume(self, mute): - """Send mute command.""" - _LOGGER.debug("Mute volume: %s", mute) - self._zone.set_mute(mute) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - _LOGGER.debug("Volume level: %.2f / %d", volume, volume * self.volume_max) - self._zone.set_volume(volume * self.volume_max) - - def select_source(self, source): - """Send the media player the command to select input source.""" - _LOGGER.debug("select_source: %s", source) - self.status = STATE_UNKNOWN - self._zone.set_input(source) - - def new_media_status(self, status): - """Handle updates of the media status.""" - _LOGGER.debug("new media_status arrived") - self.media_status = status - self.media_status_received = dt_util.utcnow() + return None diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json new file mode 100644 index 00000000000..e2261882222 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "MusicCast: {name}", + "step": { + "user": { + "description": "Set up MusicCast to integrate with Home Assistant.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "yxc_control_url_missing": "The control URL is not given in the ssdp description." + }, + "error": { + "no_musiccast_device": "This device seems to be no MusicCast Device." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json new file mode 100644 index 00000000000..4c7f3b45f0b --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "yxc_control_url_missing": "The control URL is not given in the ssdp description." + }, + "error": { + "no_musiccast_device": "This device seems to be no MusicCast Device." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Set up MusicCast to integrate with Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 442d6e9be08..6504de85199 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -289,6 +289,7 @@ FLOWS = [ "xbox", "xiaomi_aqara", "xiaomi_miio", + "yamaha_musiccast", "yeelight", "zerproc", "zha", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 0f6c01a0605..1638d932e89 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -215,5 +215,10 @@ SSDP = { { "manufacturer": "All Automacao Ltda" } + ], + "yamaha_musiccast": [ + { + "manufacturer": "Yamaha Corporation" + } ] } diff --git a/requirements_all.txt b/requirements_all.txt index cec3e65f1f5..c1f622049da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,6 +208,9 @@ aiolyric==1.0.7 # homeassistant.components.modern_forms aiomodernforms==0.1.5 +# homeassistant.components.yamaha_musiccast +aiomusiccast==0.6 + # homeassistant.components.keyboard_remote aionotify==0.2.0 @@ -1585,9 +1588,6 @@ pymonoprice==0.3 # homeassistant.components.msteams pymsteams==0.1.12 -# homeassistant.components.yamaha_musiccast -pymusiccast==0.1.6 - # homeassistant.components.myq pymyq==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adf047b3a4a..9704e3520e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,6 +133,9 @@ aiolyric==1.0.7 # homeassistant.components.modern_forms aiomodernforms==0.1.5 +# homeassistant.components.yamaha_musiccast +aiomusiccast==0.6 + # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/yamaha_musiccast/__init__.py b/tests/components/yamaha_musiccast/__init__.py new file mode 100644 index 00000000000..d5b10774c10 --- /dev/null +++ b/tests/components/yamaha_musiccast/__init__.py @@ -0,0 +1 @@ +"""Tests for the MusicCast integration.""" diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py new file mode 100644 index 00000000000..6f5709ec7cc --- /dev/null +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -0,0 +1,287 @@ +"""Test config flow.""" + +from unittest.mock import patch + +from aiomusiccast import MusicCastConnectionException +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp +from homeassistant.components.yamaha_musiccast.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_setup_entry(): + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.yamaha_musiccast.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture +def mock_get_device_info_valid(): + """Mock getting valid device info from musiccast API.""" + with patch( + "aiomusiccast.MusicCastDevice.get_device_info", + return_value={"system_id": "1234567890", "model_name": "MC20"}, + ): + yield + + +@pytest.fixture +def mock_get_device_info_invalid(): + """Mock getting invalid device info from musiccast API.""" + with patch( + "aiomusiccast.MusicCastDevice.get_device_info", + return_value={"type": "no_yamaha"}, + ): + yield + + +@pytest.fixture +def mock_get_device_info_exception(): + """Mock raising an unexpected Exception.""" + with patch( + "aiomusiccast.MusicCastDevice.get_device_info", + side_effect=Exception("mocked error"), + ): + yield + + +@pytest.fixture +def mock_get_device_info_mc_exception(): + """Mock raising an unexpected Exception.""" + with patch( + "aiomusiccast.MusicCastDevice.get_device_info", + side_effect=MusicCastConnectionException("mocked error"), + ): + yield + + +@pytest.fixture +def mock_ssdp_yamaha(): + """Mock that the SSDP detected device is a musiccast device.""" + with patch("aiomusiccast.MusicCastDevice.check_yamaha_ssdp", return_value=True): + yield + + +@pytest.fixture +def mock_ssdp_no_yamaha(): + """Mock that the SSDP detected device is not a musiccast device.""" + with patch("aiomusiccast.MusicCastDevice.check_yamaha_ssdp", return_value=False): + yield + + +# User Flows + + +async def test_user_input_device_not_found(hass, mock_get_device_info_mc_exception): + """Test when user specifies a non-existing device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "none"}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_user_input_non_yamaha_device_found(hass, mock_get_device_info_invalid): + """Test when user specifies an existing device, which does not provide the musiccast API.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "127.0.0.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_musiccast_device"} + + +async def test_user_input_device_already_existing(hass, mock_get_device_info_valid): + """Test when user specifies an existing device.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1234567890", + data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"}, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "192.168.188.18"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_user_input_unknown_error(hass, mock_get_device_info_exception): + """Test when user specifies an existing device, which does not provide the musiccast API.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "127.0.0.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_user_input_device_found(hass, mock_get_device_info_valid): + """Test when user specifies an existing device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "127.0.0.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert isinstance(result2["result"], ConfigEntry) + assert result2["data"] == { + "host": "127.0.0.1", + "serial": "1234567890", + } + + +async def test_import_device_already_existing(hass, mock_get_device_info_valid): + """Test when the configurations.yaml contains an existing device.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1234567890", + data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"}, + ) + mock_entry.add_to_hass(hass) + + config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_error(hass, mock_get_device_info_exception): + """Test when in the configuration.yaml a device is configured, which cannot be added..""" + config = {"platform": "yamaha_musiccast", "host": "192.168.188.18", "port": 5006} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_import_device_successful(hass, mock_get_device_info_valid): + """Test when the device was imported successfully.""" + config = {"platform": "yamaha_musiccast", "host": "127.0.0.1", "port": 5006} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert isinstance(result["result"], ConfigEntry) + assert result["data"] == { + "host": "127.0.0.1", + "serial": "1234567890", + } + + +# SSDP Flows + + +async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha): + """Test when an SSDP discovered device is not a musiccast device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "123456789", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "yxc_control_url_missing" + + +async def test_ssdp_discovery_successful_add_device(hass, mock_ssdp_yamaha): + """Test when the SSDP discovered device is a musiccast device and the user confirms it.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "1234567890", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert isinstance(result2["result"], ConfigEntry) + assert result2["data"] == { + "host": "127.0.0.1", + "serial": "1234567890", + } + + +async def test_ssdp_discovery_existing_device_update(hass, mock_ssdp_yamaha): + """Test when the SSDP discovered device is a musiccast device, but it already exists with another IP.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1234567890", + data={CONF_HOST: "192.168.188.18", "model": "MC20", "serial": "1234567890"}, + ) + mock_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1/desc.xml", + ssdp.ATTR_UPNP_MODEL_NAME: "MC20", + ssdp.ATTR_UPNP_SERIAL: "1234567890", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_entry.data[CONF_HOST] == "127.0.0.1" From 77f6d1f5cb21b21db43573b8d4477db905dc3b6a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 14 Jun 2021 10:56:24 +0200 Subject: [PATCH 344/750] Do not return an exception in modbus (#51829) --- homeassistant/components/modbus/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index ce4ea8235b4..495a22f8180 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -326,7 +326,7 @@ class ModbusHub: result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs) except ModbusException as exception_error: self._log_error(str(exception_error)) - result = exception_error + return None if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]): self._log_error(str(result)) return None From 08af791d4cf4eff68adaf1f1cf4e6a79febb1df3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 13:05:27 +0200 Subject: [PATCH 345/750] Improve editing of device conditions referencing non-added alarm (#51830) --- .../alarm_control_panel/device_condition.py | 10 +- .../test_device_condition.py | 131 +++++++----------- 2 files changed, 49 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index db010051010..3cbaa019ad0 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -13,7 +13,6 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, @@ -29,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -70,13 +70,7 @@ async def async_get_conditions( if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - - # We need a state or else we can't populate the different armed conditions - if state is None: - continue - - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) # Add conditions for each entity that belongs to this integration base_condition = { diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index 450393135af..b1e2c171cea 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -1,7 +1,7 @@ """The tests for Alarm control panel device conditions.""" import pytest -from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel import DOMAIN, const import homeassistant.components.automation as automation from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -43,7 +43,30 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_no_conditions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_condition_types", + [ + (False, 0, 0, []), + (False, const.SUPPORT_ALARM_ARM_AWAY, 0, ["is_armed_away"]), + (False, const.SUPPORT_ALARM_ARM_HOME, 0, ["is_armed_home"]), + (False, const.SUPPORT_ALARM_ARM_NIGHT, 0, ["is_armed_night"]), + (False, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, 0, ["is_armed_custom_bypass"]), + (True, 0, 0, []), + (True, 0, const.SUPPORT_ALARM_ARM_AWAY, ["is_armed_away"]), + (True, 0, const.SUPPORT_ALARM_ARM_HOME, ["is_armed_home"]), + (True, 0, const.SUPPORT_ALARM_ARM_NIGHT, ["is_armed_night"]), + (True, 0, const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, ["is_armed_custom_bypass"]), + ], +) +async def test_get_conditions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_condition_types, +): """Test we get the expected conditions from a alarm_control_panel.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -51,101 +74,41 @@ async def test_get_no_conditions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, []) - - -async def test_get_minimum_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a alarm_control_panel.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 0} - ) - expected_conditions = [ + if set_state: + hass.states.async_set( + "alarm_control_panel.test_5678", + "attributes", + {"supported_features": features_state}, + ) + expected_conditions = [] + basic_condition_types = ["is_disarmed", "is_triggered"] + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_disarmed", + "type": condition, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_triggered", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, + } + for condition in basic_condition_types ] - - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, expected_conditions) - - -async def test_get_maximum_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a alarm_control_panel.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 31} - ) - expected_conditions = [ + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_disarmed", + "type": condition, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_triggered", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_armed_home", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_armed_away", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_armed_night", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_armed_custom_bypass", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, + } + for condition in expected_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) From fca79237c3dfc3e7adefff05afccca555a85d745 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 14 Jun 2021 08:28:57 -0300 Subject: [PATCH 346/750] Create dataclass to mock entry setup in Broadlink tests (#50134) --- tests/components/broadlink/__init__.py | 14 ++- tests/components/broadlink/test_device.py | 121 ++++++++++--------- tests/components/broadlink/test_heartbeat.py | 8 +- tests/components/broadlink/test_remote.py | 28 ++--- tests/components/broadlink/test_sensors.py | 70 +++++++---- 5 files changed, 138 insertions(+), 103 deletions(-) diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 780887551f2..c65870add96 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -1,4 +1,5 @@ """Tests for the Broadlink integration.""" +from dataclasses import dataclass from unittest.mock import MagicMock, patch from homeassistant.components.broadlink.const import DOMAIN @@ -70,6 +71,15 @@ BROADLINK_DEVICES = { } +@dataclass +class MockSetup: + """Representation of a mock setup.""" + + api: MagicMock + entry: MockConfigEntry + factory: MagicMock + + class BroadlinkDevice: """Representation of a Broadlink device.""" @@ -96,11 +106,11 @@ class BroadlinkDevice: with patch( "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api, - ): + ) as mock_factory: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - return mock_api, mock_entry + return MockSetup(mock_api, mock_entry, mock_factory) def get_mock_api(self): """Return a mock device (API).""" diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 2ee8f7a5218..5430af9e311 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -12,6 +12,8 @@ from . import get_device from tests.common import mock_device_registry, mock_registry +DEVICE_FACTORY = "homeassistant.components.broadlink.device.blk.gendevice" + async def test_device_setup(hass): """Test a successful setup.""" @@ -22,13 +24,15 @@ async def test_device_setup(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) + + assert mock_setup.entry.state is ConfigEntryState.LOADED + assert mock_setup.api.auth.call_count == 1 + assert mock_setup.api.get_fwversion.call_count == 1 + assert mock_setup.factory.call_count == 1 - assert mock_entry.state == ConfigEntryState.LOADED - assert mock_api.auth.call_count == 1 - assert mock_api.get_fwversion.call_count == 1 forward_entries = {c[1][1] for c in mock_forward.mock_calls} - domains = get_domains(mock_api.type) + domains = get_domains(mock_setup.api.type) assert mock_forward.call_count == len(domains) assert forward_entries == domains assert mock_init.call_count == 0 @@ -45,10 +49,10 @@ async def test_device_setup_authentication_error(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ConfigEntryState.SETUP_ERROR - assert mock_api.auth.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR + assert mock_setup.api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 1 assert mock_init.mock_calls[0][2]["context"]["source"] == "reauth" @@ -69,10 +73,10 @@ async def test_device_setup_network_timeout(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_RETRY - assert mock_api.auth.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY + assert mock_setup.api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -88,10 +92,10 @@ async def test_device_setup_os_error(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_RETRY - assert mock_api.auth.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY + assert mock_setup.api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -107,10 +111,10 @@ async def test_device_setup_broadlink_exception(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_ERROR - assert mock_api.auth.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR + assert mock_setup.api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -126,11 +130,11 @@ async def test_device_setup_update_network_timeout(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_RETRY - assert mock_api.auth.call_count == 1 - assert mock_api.check_sensors.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY + assert mock_setup.api.auth.call_count == 1 + assert mock_setup.api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -149,11 +153,12 @@ async def test_device_setup_update_authorization_error(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) + + assert mock_setup.entry.state is ConfigEntryState.LOADED + assert mock_setup.api.auth.call_count == 2 + assert mock_setup.api.check_sensors.call_count == 2 - assert mock_entry.state is ConfigEntryState.LOADED - assert mock_api.auth.call_count == 2 - assert mock_api.check_sensors.call_count == 2 forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -173,11 +178,11 @@ async def test_device_setup_update_authentication_error(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_RETRY - assert mock_api.auth.call_count == 2 - assert mock_api.check_sensors.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY + assert mock_setup.api.auth.call_count == 2 + assert mock_setup.api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 1 assert mock_init.mock_calls[0][2]["context"]["source"] == "reauth" @@ -198,11 +203,11 @@ async def test_device_setup_update_broadlink_exception(hass): ) as mock_forward, patch.object( hass.config_entries.flow, "async_init" ) as mock_init: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.SETUP_RETRY - assert mock_api.auth.call_count == 1 - assert mock_api.check_sensors.call_count == 1 + assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY + assert mock_setup.api.auth.call_count == 1 + assert mock_setup.api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -214,11 +219,11 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass): mock_api.get_fwversion.side_effect = blke.BroadlinkException() with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.LOADED + assert mock_setup.entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} - domains = get_domains(mock_api.type) + domains = get_domains(mock_setup.api.type) assert mock_forward.call_count == len(domains) assert forward_entries == domains @@ -230,11 +235,11 @@ async def test_device_setup_get_fwversion_os_error(hass): mock_api.get_fwversion.side_effect = OSError() with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: - _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state is ConfigEntryState.LOADED + assert mock_setup.entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} - domains = get_domains(mock_api.type) + domains = get_domains(mock_setup.api.type) assert mock_forward.call_count == len(domains) assert forward_entries == domains @@ -246,12 +251,14 @@ async def test_device_setup_registry(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - _, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) await hass.async_block_till_done() assert len(device_registry.devices) == 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) assert device_entry.identifiers == {(DOMAIN, device.mac)} assert device_entry.name == device.name assert device_entry.model == device.model @@ -267,16 +274,16 @@ async def test_device_unload_works(hass): device = get_device("Office") with patch.object(hass.config_entries, "async_forward_entry_setup"): - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True ) as mock_forward: - await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.config_entries.async_unload(mock_setup.entry.entry_id) - assert mock_entry.state is ConfigEntryState.NOT_LOADED + assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} - domains = get_domains(mock_api.type) + domains = get_domains(mock_setup.api.type) assert mock_forward.call_count == len(domains) assert forward_entries == domains @@ -290,14 +297,14 @@ async def test_device_unload_authentication_error(hass): with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object( hass.config_entries.flow, "async_init" ): - _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True ) as mock_forward: - await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.config_entries.async_unload(mock_setup.entry.entry_id) - assert mock_entry.state is ConfigEntryState.NOT_LOADED + assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 @@ -308,14 +315,14 @@ async def test_device_unload_update_failed(hass): mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() with patch.object(hass.config_entries, "async_forward_entry_setup"): - _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True ) as mock_forward: - await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.config_entries.async_unload(mock_setup.entry.entry_id) - assert mock_entry.state is ConfigEntryState.NOT_LOADED + assert mock_setup.entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 @@ -326,16 +333,16 @@ async def test_device_update_listener(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.broadlink.device.blk.gendevice", return_value=mock_api - ): - hass.config_entries.async_update_entry(mock_entry, title="New Name") + with patch(DEVICE_FACTORY, return_value=mock_setup.api): + hass.config_entries.async_update_entry(mock_setup.entry, title="New Name") await hass.async_block_till_done() - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) assert device_entry.name == "New Name" for entry in async_entries_for_device(entity_registry, device_entry.id): assert entry.original_name.startswith("New Name") diff --git a/tests/components/broadlink/test_heartbeat.py b/tests/components/broadlink/test_heartbeat.py index 8e52a562425..de47a16c0b9 100644 --- a/tests/components/broadlink/test_heartbeat.py +++ b/tests/components/broadlink/test_heartbeat.py @@ -72,10 +72,10 @@ async def test_heartbeat_unload(hass): """Test that the heartbeat is deactivated when the last config entry is removed.""" device = get_device("Office") - _, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) await hass.async_block_till_done() - await hass.config_entries.async_remove(mock_entry.entry_id) + await hass.config_entries.async_remove(mock_setup.entry.entry_id) await hass.async_block_till_done() with patch(DEVICE_PING) as mock_ping: @@ -91,11 +91,11 @@ async def test_heartbeat_do_not_unload(hass): device_a = get_device("Office") device_b = get_device("Bedroom") - _, mock_entry_a = await device_a.setup_entry(hass) + mock_setup = await device_a.setup_entry(hass) await device_b.setup_entry(hass) await hass.async_block_till_done() - await hass.config_entries.async_remove(mock_entry_a.entry_id) + await hass.config_entries.async_remove(mock_setup.entry.entry_id) await hass.async_block_till_done() with patch(DEVICE_PING) as mock_ping: diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 2d21b588c33..abc500479ea 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -28,10 +28,10 @@ async def test_remote_setup_works(hass): for device in map(get_device, REMOTE_DEVICES): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)} + {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -40,7 +40,7 @@ async def test_remote_setup_works(hass): remote = remotes.pop() assert remote.original_name == f"{device.name} Remote" assert hass.states.get(remote.entity_id).state == STATE_ON - assert mock_api.auth.call_count == 1 + assert mock_setup.api.auth.call_count == 1 async def test_remote_send_command(hass): @@ -48,10 +48,10 @@ async def test_remote_send_command(hass): for device in map(get_device, REMOTE_DEVICES): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)} + {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -65,9 +65,9 @@ async def test_remote_send_command(hass): blocking=True, ) - assert mock_api.send_data.call_count == 1 - assert mock_api.send_data.call_args == call(b64decode(IR_PACKET)) - assert mock_api.auth.call_count == 1 + assert mock_setup.api.send_data.call_count == 1 + assert mock_setup.api.send_data.call_args == call(b64decode(IR_PACKET)) + assert mock_setup.api.auth.call_count == 1 async def test_remote_turn_off_turn_on(hass): @@ -75,10 +75,10 @@ async def test_remote_turn_off_turn_on(hass): for device in map(get_device, REMOTE_DEVICES): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass) + mock_setup = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)} + {(DOMAIN, mock_setup.entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -99,7 +99,7 @@ async def test_remote_turn_off_turn_on(hass): {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, ) - assert mock_api.send_data.call_count == 0 + assert mock_setup.api.send_data.call_count == 0 await hass.services.async_call( REMOTE_DOMAIN, @@ -115,6 +115,6 @@ async def test_remote_turn_off_turn_on(hass): {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, ) - assert mock_api.send_data.call_count == 1 - assert mock_api.send_data.call_args == call(b64decode(IR_PACKET)) - assert mock_api.auth.call_count == 1 + assert mock_setup.api.send_data.call_count == 1 + assert mock_setup.api.send_data.call_args == call(b64decode(IR_PACKET)) + assert mock_setup.api.auth.call_count == 1 diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 5cc75c28a73..1f8f913cfe4 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -22,10 +22,12 @@ async def test_a1_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors_raw.call_count == 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 @@ -58,14 +60,16 @@ async def test_a1_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 - mock_api.check_sensors_raw.return_value = { + mock_setup.api.check_sensors_raw.return_value = { "temperature": 22.5, "humidity": 47.4, "air_quality": 2, @@ -75,7 +79,7 @@ async def test_a1_sensor_update(hass): await hass.helpers.entity_component.async_update_entity( next(iter(sensors)).entity_id ) - assert mock_api.check_sensors_raw.call_count == 2 + assert mock_setup.api.check_sensors_raw.call_count == 2 sensors_and_states = { (sensor.original_name, hass.states.get(sensor.entity_id).state) @@ -99,10 +103,12 @@ async def test_rm_pro_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 @@ -123,18 +129,20 @@ async def test_rm_pro_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 - mock_api.check_sensors.return_value = {"temperature": 25.8} + mock_setup.api.check_sensors.return_value = {"temperature": 25.8} await hass.helpers.entity_component.async_update_entity( next(iter(sensors)).entity_id ) - assert mock_api.check_sensors.call_count == 2 + assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { (sensor.original_name, hass.states.get(sensor.entity_id).state) @@ -155,18 +163,20 @@ async def test_rm_pro_filter_crazy_temperature(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 - mock_api.check_sensors.return_value = {"temperature": -7} + mock_setup.api.check_sensors.return_value = {"temperature": -7} await hass.helpers.entity_component.async_update_entity( next(iter(sensors)).entity_id ) - assert mock_api.check_sensors.call_count == 2 + assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { (sensor.original_name, hass.states.get(sensor.entity_id).state) @@ -184,10 +194,12 @@ async def test_rm_mini3_no_sensor(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 0 @@ -202,10 +214,12 @@ async def test_rm4_pro_hts2_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 @@ -229,18 +243,20 @@ async def test_rm4_pro_hts2_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 - mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} + mock_setup.api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} await hass.helpers.entity_component.async_update_entity( next(iter(sensors)).entity_id ) - assert mock_api.check_sensors.call_count == 2 + assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { (sensor.original_name, hass.states.get(sensor.entity_id).state) @@ -261,10 +277,12 @@ async def test_rm4_pro_no_sensor(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 From 33ec6621c7db4dca888ebf05b64a363de3ed86fd Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 14 Jun 2021 21:51:11 +1000 Subject: [PATCH 347/750] Bump georss_ign_sismologia_client to v0.3 (#51838) --- homeassistant/components/ign_sismologia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index ce472e66449..e80e3a4eeec 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -2,7 +2,7 @@ "domain": "ign_sismologia", "name": "IGN Sismolog\u00eda", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", - "requirements": ["georss_ign_sismologia_client==0.2"], + "requirements": ["georss_ign_sismologia_client==0.3"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c1f622049da..8a209b74cff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -660,7 +660,7 @@ geopy==2.1.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.2 +georss_ign_sismologia_client==0.3 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9704e3520e7..838658bc714 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -363,7 +363,7 @@ geopy==2.1.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.2 +georss_ign_sismologia_client==0.3 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 From af0554374462e2ce6d7c42e8ebb559021c3638e2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 14:12:42 +0200 Subject: [PATCH 348/750] Improve editing of device conditions referencing non-added humidifier (#51834) --- .../components/humidifier/device_condition.py | 18 +- .../humidifier/test_device_condition.py | 369 +++++++++++------- 2 files changed, 244 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 02a667f2f68..f2bf032b195 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -7,16 +7,16 @@ from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, - ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.entity import get_capability, get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, const @@ -48,9 +48,9 @@ async def async_get_conditions( if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) + supported_features = get_supported_features(hass, entry.entity_id) - if state and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES: + if supported_features & const.SUPPORT_MODES: conditions.append( { CONF_CONDITION: "device", @@ -87,15 +87,17 @@ def async_condition_from_config( async def async_get_condition_capabilities(hass, config): """List condition capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) condition_type = config[CONF_TYPE] fields = {} if condition_type == "is_mode": - if state: - modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, []) - else: + try: + modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_AVAILABLE_MODES) + or [] + ) + except HomeAssistantError: modes = [] fields[vol.Required(ATTR_MODE)] = vol.In(modes) diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index 59887f65a33..0d0f65d2c97 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -11,7 +11,6 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, - async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, mock_device_registry, @@ -38,7 +37,24 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_condition_types", + [ + (False, 0, 0, []), + (False, const.SUPPORT_MODES, 0, ["is_mode"]), + (True, 0, 0, []), + (True, 0, const.SUPPORT_MODES, ["is_mode"]), + ], +) +async def test_get_conditions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_condition_types, +): """Test we get the expected conditions from a humidifier.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -46,80 +62,38 @@ async def test_get_conditions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - f"{DOMAIN}.test_5678", - STATE_ON, - { - ATTR_MODE: const.MODE_AWAY, - const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY], - }, + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - hass.states.async_set( - "humidifier.test_5678", "attributes", {"supported_features": 1} - ) - expected_conditions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + expected_conditions = [] + basic_condition_types = ["is_on", "is_off"] + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_off", + "type": condition, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_mode", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, + } + for condition in basic_condition_types ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, expected_conditions) - - -async def test_get_conditions_toggle_only(hass, device_reg, entity_reg): - """Test we get the expected conditions from a humidifier.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - f"{DOMAIN}.test_5678", - STATE_ON, - { - ATTR_MODE: const.MODE_AWAY, - const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY], - }, - ) - hass.states.async_set( - "humidifier.test_5678", "attributes", {"supported_features": 0} - ) - expected_conditions = [ + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_off", + "type": condition, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, + } + for condition in expected_condition_types ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) @@ -227,81 +201,206 @@ async def test_if_state(hass, calls): assert len(calls) == 3 -async def test_capabilities(hass): - """Test capabilities.""" - hass.states.async_set( - "humidifier.entity", - STATE_ON, - { - ATTR_MODE: const.MODE_AWAY, - const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY], - }, - ) - - # Test mode - capabilities = await device_condition.async_get_condition_capabilities( - hass, - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": "humidifier.entity", - "type": "is_mode", - }, - ) - - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ - { - "name": "mode", - "options": [("home", "home"), ("away", "away")], - "required": True, - "type": "select", - } - ] - - -async def test_capabilities_no_state(hass): - """Test capabilities while state not available.""" - # Test mode - capabilities = await device_condition.async_get_condition_capabilities( - hass, - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": "humidifier.entity", - "type": "is_mode", - }, - ) - - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "mode", "options": [], "required": True, "type": "select"}] - - -async def test_get_condition_capabilities(hass, device_reg, entity_reg): - """Test we get the expected toggle capabilities.""" +@pytest.mark.parametrize( + "set_state,capabilities_reg,capabilities_state,condition,expected_capabilities", + [ + ( + False, + {}, + {}, + "is_mode", + [ + { + "name": "mode", + "options": [], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]}, + {}, + "is_mode", + [ + { + "name": "mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {}, + {}, + "is_off", + [ + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + } + ], + ), + ( + False, + {}, + {}, + "is_on", + [ + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + } + ], + ), + ( + True, + {}, + {}, + "is_mode", + [ + { + "name": "mode", + "options": [], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]}, + "is_mode", + [ + { + "name": "mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {}, + "is_off", + [ + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + } + ], + ), + ( + True, + {}, + {}, + "is_on", + [ + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + } + ], + ), + ], +) +async def test_capabilities( + hass, + device_reg, + entity_reg, + set_state, + capabilities_reg, + capabilities_state, + condition, + expected_capabilities, +): + """Test getting capabilities.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_capabilities = { - "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} - ] - } - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - for condition in conditions: - capabilities = await async_get_device_automation_capabilities( - hass, "condition", condition + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + capabilities=capabilities_reg, + ) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + STATE_ON, + capabilities_state, ) - assert capabilities == expected_capabilities + + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": f"{DOMAIN}.test_5678", + "type": condition, + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) + + +@pytest.mark.parametrize( + "condition,capability_name,extra", + [ + ("is_mode", "mode", {"type": "select", "options": []}), + ], +) +async def test_capabilities_missing_entity( + hass, device_reg, entity_reg, condition, capability_name, extra +): + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": f"{DOMAIN}.test_5678", + "type": condition, + }, + ) + + expected_capabilities = [ + { + "name": capability_name, + "required": True, + **extra, + } + ] + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) From 59ef55c34f26d2f2fa681052d25d039d72482f3d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 14:34:59 +0200 Subject: [PATCH 349/750] Improve editing of device conditions referencing non-added cover (#51833) --- .../components/cover/device_condition.py | 8 +- .../components/cover/test_device_condition.py | 198 ++++-------------- 2 files changed, 48 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 148e2fb48b0..bd433dbd93d 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_ABOVE, CONF_BELOW, CONF_CONDITION, @@ -28,6 +27,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ( @@ -77,11 +77,7 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: - continue - - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = get_supported_features(hass, entry.entity_id) supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) # Add conditions for each entity that belongs to this integration diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 8e8d92e5a1c..cf05a112a0a 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -2,7 +2,13 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.cover import DOMAIN +from homeassistant.components.cover import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) from homeassistant.const import ( CONF_PLATFORM, STATE_CLOSED, @@ -43,65 +49,31 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): - """Test we get the expected conditions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_open", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, expected_conditions) - - -async def test_get_conditions_set_pos( - hass, device_reg, entity_reg, enable_custom_integrations +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_condition_types", + [ + (False, 0, 0, []), + (False, SUPPORT_CLOSE, 0, ["is_open", "is_closed", "is_opening", "is_closing"]), + (False, SUPPORT_OPEN, 0, ["is_open", "is_closed", "is_opening", "is_closing"]), + (False, SUPPORT_SET_POSITION, 0, ["is_position"]), + (False, SUPPORT_SET_TILT_POSITION, 0, ["is_tilt_position"]), + (True, 0, 0, []), + (True, 0, SUPPORT_CLOSE, ["is_open", "is_closed", "is_opening", "is_closing"]), + (True, 0, SUPPORT_OPEN, ["is_open", "is_closed", "is_opening", "is_closing"]), + (True, 0, SUPPORT_SET_POSITION, ["is_position"]), + (True, 0, SUPPORT_SET_TILT_POSITION, ["is_tilt_position"]), + ], +) +async def test_get_conditions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_condition_types, ): """Test we get the expected conditions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] - config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -109,106 +81,28 @@ async def test_get_conditions_set_pos( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + await hass.async_block_till_done() - expected_conditions = [ + expected_conditions = [] + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_open", + "type": condition, "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_position", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, expected_conditions) - - -async def test_get_conditions_set_tilt_pos( - hass, device_reg, entity_reg, enable_custom_integrations -): - """Test we get the expected conditions from a cover.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[2] - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, "test", ent.unique_id, device_id=device_entry.id - ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_open", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closed", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_opening", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_closing", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_tilt_position", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_{ent.unique_id}", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for condition in expected_condition_types ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) From 06fc21e287046875934038e0d818c16b44fa27e4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 15:22:31 +0200 Subject: [PATCH 350/750] Improve editing of device conditions referencing non-added sensor (#51835) --- .../components/sensor/device_condition.py | 30 +++----- .../sensor/test_device_condition.py | 76 +++++++++++++++++-- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 4d3d8a4b477..a77ed2d2cd7 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -7,8 +7,6 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -27,8 +25,9 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity import get_device_class, get_unit_of_measurement from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry, @@ -116,18 +115,12 @@ async def async_get_conditions( ] for entry in entries: - device_class = DEVICE_CLASS_NONE - state = hass.states.get(entry.entity_id) - unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None - ) + device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE + unit_of_measurement = get_unit_of_measurement(hass, entry.entity_id) - if not state or not unit_of_measurement: + if not unit_of_measurement: continue - if ATTR_DEVICE_CLASS in state.attributes: - device_class = state.attributes[ATTR_DEVICE_CLASS] - templates = ENTITY_CONDITIONS.get( device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] ) @@ -167,15 +160,14 @@ def async_condition_from_config( async def async_get_condition_capabilities(hass, config): """List condition capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) - unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None - ) + try: + unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID]) + except HomeAssistantError: + unit_of_measurement = None - if not state or not unit_of_measurement: + if not unit_of_measurement: raise InvalidDeviceAutomationConfig( - "No state or unit of measurement found for " - f"condition entity {config[CONF_ENTITY_ID]}" + "No unit of measurement found for condition entity {config[CONF_ENTITY_ID]}" ) return { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 6cad21c5bde..daf452cf715 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -4,7 +4,12 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import ( + CONF_PLATFORM, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + STATE_UNKNOWN, +) from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -80,8 +85,60 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr assert conditions == expected_conditions +async def test_get_conditions_no_state(hass, device_reg, entity_reg): + """Test we get the expected conditions from a sensor.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_ids = {} + for device_class in DEVICE_CLASSES: + entity_ids[device_class] = entity_reg.async_get_or_create( + DOMAIN, + "test", + f"5678_{device_class}", + device_id=device_entry.id, + device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), + ).entity_id + + await hass.async_block_till_done() + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": entity_ids[device_class], + } + for device_class in DEVICE_CLASSES + if device_class in UNITS_OF_MEASUREMENT + for condition in ENTITY_CONDITIONS[device_class] + if device_class != "none" + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +@pytest.mark.parametrize( + "set_state,device_class_reg,device_class_state,unit_reg,unit_state", + [ + (False, DEVICE_CLASS_BATTERY, None, PERCENTAGE, None), + (True, None, DEVICE_CLASS_BATTERY, None, PERCENTAGE), + ], +) async def test_get_condition_capabilities( - hass, device_reg, entity_reg, enable_custom_integrations + hass, + device_reg, + entity_reg, + set_state, + device_class_reg, + device_class_state, + unit_reg, + unit_state, ): """Test we get the expected capabilities from a sensor condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -93,15 +150,20 @@ async def test_get_condition_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create( + entity_id = entity_reg.async_get_or_create( DOMAIN, "test", platform.ENTITIES["battery"].unique_id, device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() + device_class=device_class_reg, + unit_of_measurement=unit_reg, + ).entity_id + if set_state: + hass.states.async_set( + entity_id, + None, + {"device_class": device_class_state, "unit_of_measurement": unit_state}, + ) expected_capabilities = { "extra_fields": [ From a989677bef1a79b66a017c4643973c735989f9cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 15:26:46 +0200 Subject: [PATCH 351/750] Improve editing of device conditions referencing non-added binary sensor (#51831) * Improve editing of device conditions referencing non-added binary sensor * Update tests --- .../binary_sensor/device_condition.py | 8 ++--- .../binary_sensor/test_device_condition.py | 35 +++++++++++++++++++ .../binary_sensor/test_device_trigger.py | 6 ++-- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 8c506634200..eed5c3f5896 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -4,9 +4,10 @@ from __future__ import annotations import voluptuous as vol from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry, @@ -216,10 +217,7 @@ async def async_get_conditions( ] for entry in entries: - device_class = DEVICE_CLASS_NONE - state = hass.states.get(entry.entity_id) - if state and ATTR_DEVICE_CLASS in state.attributes: - device_class = state.attributes[ATTR_DEVICE_CLASS] + device_class = get_device_class(hass, entry.entity_id) or DEVICE_CLASS_NONE templates = ENTITY_CONDITIONS.get( device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 5d8673825fc..3d1b694c7ce 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -78,6 +78,41 @@ async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integr assert conditions == expected_conditions +async def test_get_conditions_no_state(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_ids = {} + for device_class in DEVICE_CLASSES: + entity_ids[device_class] = entity_reg.async_get_or_create( + DOMAIN, + "test", + f"5678_{device_class}", + device_id=device_entry.id, + device_class=device_class, + ).entity_id + + await hass.async_block_till_done() + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": entity_ids[device_class], + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + async def test_get_condition_capabilities(hass, device_reg, entity_reg): """Test we get the expected capabilities from a binary_sensor condition.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 1dbed7d19e1..8bd80be6524 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -78,9 +78,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrat assert triggers == expected_triggers -async def test_get_triggers_no_state( - hass, device_reg, entity_reg, enable_custom_integrations -): +async def test_get_triggers_no_state(hass, device_reg, entity_reg): """Test we get the expected triggers from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -96,7 +94,7 @@ async def test_get_triggers_no_state( entity_ids[device_class] = entity_reg.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + f"5678_{device_class}", device_id=device_entry.id, device_class=device_class, ).entity_id From 4c9f12b9c5a9fdeffb2a3b6b55ea3c6f209b3cec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Jun 2021 17:09:20 +0200 Subject: [PATCH 352/750] Correct trace path for trigger with custom id (#51847) --- homeassistant/components/arcam_fmj/device_trigger.py | 4 ++-- homeassistant/components/automation/__init__.py | 4 ++-- homeassistant/components/geo_location/trigger.py | 4 ++-- homeassistant/components/homeassistant/triggers/event.py | 4 ++-- .../components/homeassistant/triggers/homeassistant.py | 6 +++--- .../components/homeassistant/triggers/numeric_state.py | 4 ++-- homeassistant/components/homeassistant/triggers/state.py | 4 ++-- homeassistant/components/homeassistant/triggers/time.py | 4 ++-- .../components/homeassistant/triggers/time_pattern.py | 4 ++-- .../components/homekit_controller/device_trigger.py | 6 ++++-- homeassistant/components/kodi/device_trigger.py | 4 ++-- homeassistant/components/litejet/trigger.py | 4 ++-- homeassistant/components/mqtt/trigger.py | 4 ++-- homeassistant/components/philips_js/device_trigger.py | 4 ++-- homeassistant/components/sun/trigger.py | 4 ++-- homeassistant/components/tag/trigger.py | 4 ++-- homeassistant/components/template/trigger.py | 4 ++-- homeassistant/components/webhook/trigger.py | 8 ++++---- homeassistant/components/zone/trigger.py | 4 ++-- homeassistant/helpers/trigger.py | 4 +++- 20 files changed, 46 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 25ad5a1133f..383f28d7a20 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -56,7 +56,7 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} job = HassJob(action) if config[CONF_TYPE] == "turn_on": @@ -69,9 +69,9 @@ async def async_attach_trigger( job, { "trigger": { + **trigger_data, **config, "description": f"{DOMAIN} - {entity_id}", - "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index eb77880687d..1733f272229 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -475,8 +475,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): automation_trace.set_trigger_description(trigger_description) # Add initial variables as the trigger step - if "trigger" in variables and "id" in variables["trigger"]: - trigger_path = f"trigger/{variables['trigger']['id']}" + if "trigger" in variables and "idx" in variables["trigger"]: + trigger_path = f"trigger/{variables['trigger']['idx']}" else: trigger_path = "trigger" trace_element = TraceElement(variables, trigger_path) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 90621e7062c..c5e35ece593 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -37,7 +37,7 @@ def source_match(state, source): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) @@ -78,6 +78,7 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "geo_location", "source": source, "entity_id": event.data.get("entity_id"), @@ -86,7 +87,6 @@ async def async_attach_trigger(hass, config, action, automation_info): "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", - "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index ec44c861835..6b4cf520560 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -31,7 +31,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="event" ): """Listen for events based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} variables = None if automation_info: variables = automation_info.get("variables") @@ -93,10 +93,10 @@ async def async_attach_trigger( job, { "trigger": { + **trigger_data, "platform": platform_type, "event": event, "description": f"event '{event.event_type}'", - "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 3593e27b530..ea1a985139f 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -20,7 +20,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} event = config.get(CONF_EVENT) job = HassJob(action) @@ -33,10 +33,10 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "homeassistant", "event": event, "description": "Home Assistant stopping", - "id": trigger_id, } }, event.context, @@ -51,10 +51,10 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "homeassistant", "event": event, "description": "Home Assistant starting", - "id": trigger_id, } }, ) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 4ab92c36205..366f937a192 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -78,7 +78,7 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @@ -132,6 +132,7 @@ async def async_attach_trigger( job, { "trigger": { + **trigger_data, "platform": platform_type, "entity_id": entity_id, "below": below, @@ -140,7 +141,6 @@ async def async_attach_trigger( "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", - "id": trigger_id, } }, to_s.context, diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index edcf6b09a78..2c96b6be944 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -87,7 +87,7 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @@ -134,6 +134,7 @@ async def async_attach_trigger( job, { "trigger": { + **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, @@ -141,7 +142,6 @@ async def async_attach_trigger( "for": time_delta if not time_delta else period[entity], "attribute": attribute, "description": f"state of {entity}", - "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 3d7c612cc55..ff78e4c43c8 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -39,7 +39,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} entities = {} removes = [] job = HassJob(action) @@ -51,11 +51,11 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "time", "now": now, "description": description, "entity_id": entity_id, - "id": trigger_id, } }, ) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index bd56ea89663..0380e01c239 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -57,7 +57,7 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) seconds = config.get(CONF_SECONDS) @@ -76,10 +76,10 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "time_pattern", "now": now, "description": "time pattern", - "id": trigger_id, } }, ) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index d94552df52d..818b75e47d3 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -76,13 +76,15 @@ class TriggerSource: automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = ( + automation_info.get("trigger_data", {}) if automation_info else {} + ) def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: return self._hass.async_create_task( - action({"trigger": {**config, "id": trigger_id}}) + action({"trigger": {**trigger_data, **config}}) ) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 855d7f7dba9..584e465b3a6 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -67,7 +67,7 @@ def _attach_trigger( event_type, automation_info: dict, ): - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} job = HassJob(action) @callback @@ -75,7 +75,7 @@ def _attach_trigger( if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: hass.async_run_hass_job( job, - {"trigger": {**config, "description": event_type, "id": trigger_id}}, + {"trigger": {**trigger_data, **config, "description": event_type}}, event.context, ) diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 5bbd8e2f912..3a9930c5e70 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -31,7 +31,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) @@ -46,12 +46,12 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, CONF_PLATFORM: "litejet", CONF_NUMBER: number, CONF_HELD_MORE_THAN: held_more_than, CONF_HELD_LESS_THAN: held_less_than, "description": f"litejet switch #{number}", - "id": trigger_id, } }, ) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index ae184c9182a..3ee23356c3f 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -37,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} topic = config[CONF_TOPIC] wanted_payload = config.get(CONF_PAYLOAD) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -74,12 +74,12 @@ async def async_attach_trigger(hass, config, action, automation_info): if wanted_payload is None or wanted_payload == payload: data = { + **trigger_data, "platform": "mqtt", "topic": mqttmsg.topic, "payload": mqttmsg.payload, "qos": mqttmsg.qos, "description": f"mqtt topic {mqttmsg.topic}", - "id": trigger_id, } with suppress(ValueError): diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 0c45debd384..51efa643310 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -45,16 +45,16 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE | None: """Attach a trigger.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} registry: DeviceRegistry = await async_get_registry(hass) if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: variables = { "trigger": { + **trigger_data, "platform": "device", "domain": DOMAIN, "device_id": config[CONF_DEVICE_ID], "description": f"philips_js '{config[CONF_TYPE]}' event", - "id": trigger_id, } } diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 2a46f665e3c..b612934bfad 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -26,7 +26,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event @@ -41,11 +41,11 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": "sun", "event": event, "offset": offset, "description": description, - "id": trigger_id, } }, ) diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index e46e737986e..1984505f3a6 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -18,7 +18,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger(hass, config, action, automation_info): """Listen for tag_scanned events based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} tag_ids = set(config[TAG_ID]) device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None @@ -35,10 +35,10 @@ async def async_attach_trigger(hass, config, action, automation_info): job, { "trigger": { + **trigger_data, "platform": DOMAIN, "event": event, "description": "Tag scanned", - "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index c4751055962..6db25da76ab 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -31,7 +31,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="template" ): """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass time_delta = config.get(CONF_FOR) @@ -99,9 +99,9 @@ async def async_attach_trigger( "to_state": to_s, } trigger_variables = { + **trigger_data, "for": time_delta, "description": description, - "id": trigger_id, } @callback diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 687a72108da..6bb8a61eeec 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -20,7 +20,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def _handle_webhook(job, trigger_id, hass, webhook_id, request): +async def _handle_webhook(job, trigger_data, hass, webhook_id, request): """Handle incoming webhook.""" result = {"platform": "webhook", "webhook_id": webhook_id} @@ -31,20 +31,20 @@ async def _handle_webhook(job, trigger_id, hass, webhook_id, request): result["query"] = request.query result["description"] = "webhook" - result["id"] = trigger_id + result.update(**trigger_data) hass.async_run_hass_job(job, {"trigger": result}) async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} webhook_id = config.get(CONF_WEBHOOK_ID) job = HassJob(action) hass.components.webhook.async_register( automation_info["domain"], automation_info["name"], webhook_id, - partial(_handle_webhook, job, trigger_id), + partial(_handle_webhook, job, trigger_data), ) @callback diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 64c904defea..eb084fe1874 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -37,7 +37,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type: str = "zone" ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_id = automation_info.get("trigger_id") if automation_info else None + trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) @@ -74,6 +74,7 @@ async def async_attach_trigger( job, { "trigger": { + **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, @@ -81,7 +82,6 @@ async def async_attach_trigger( "zone": zone_state, "event": event, "description": description, - "id": trigger_id, } }, to_s.context, diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 64c5373d8f5..b5a82c3c020 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -75,7 +75,9 @@ async def async_initialize_triggers( for idx, conf in enumerate(trigger_config): platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") - info = {**info, "trigger_id": trigger_id} + trigger_idx = f"{idx}" + trigger_data = {"id": trigger_id, "idx": trigger_idx} + info = {**info, "trigger_data": trigger_data} triggers.append(platform.async_attach_trigger(hass, conf, action, info)) attach_results = await asyncio.gather(*triggers, return_exceptions=True) From 7cd57dd156298b87e41497bec7feaeb8c227a5ab Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 15 Jun 2021 01:29:23 +1000 Subject: [PATCH 353/750] Bump aio_geojson_geonetnz_quakes to v0.13 (#51846) --- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 64a78c02d25..5668cd6cb3f 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Quakes", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", - "requirements": ["aio_geojson_geonetnz_quakes==0.12"], + "requirements": ["aio_geojson_geonetnz_quakes==0.13"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 8a209b74cff..b735fd3e431 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -123,7 +123,7 @@ afsapi==0.0.4 agent-py==0.0.23 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.12 +aio_geojson_geonetnz_quakes==0.13 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 838658bc714..727b3cf9b46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -63,7 +63,7 @@ advantage_air==0.2.1 agent-py==0.0.23 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.12 +aio_geojson_geonetnz_quakes==0.13 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.6 From 97e77ab2293ecf239f395ac2e2ceae465acde349 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 14 Jun 2021 23:59:25 +0800 Subject: [PATCH 354/750] Improve type hints in stream (#51837) * Improve type hints in stream * Fix import locations * Add stream to .strict-typing Co-authored-by: Ruslan Sayfutdinov --- .strict-typing | 1 + homeassistant/components/stream/__init__.py | 68 ++++++++++++--------- homeassistant/components/stream/core.py | 36 +++++++---- homeassistant/components/stream/hls.py | 36 ++++++++--- homeassistant/components/stream/recorder.py | 8 +-- homeassistant/components/stream/worker.py | 16 ++--- mypy.ini | 11 ++++ tests/components/stream/test_hls.py | 18 +++--- tests/components/stream/test_recorder.py | 12 ++-- tests/components/stream/test_worker.py | 14 ++--- 10 files changed, 135 insertions(+), 85 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7218823424e..2427bcdd754 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,6 +65,7 @@ homeassistant.components.sensor.* homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* +homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.switch.* homeassistant.components.synology_dsm.* diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index f71e725fcb4..d8e4cb2cdb2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -16,16 +16,19 @@ to always keep workers active. """ from __future__ import annotations +from collections.abc import Mapping import logging import re import secrets import threading import time from types import MappingProxyType +from typing import cast from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ENDPOINTS, @@ -40,18 +43,21 @@ from .const import ( ) from .core import PROVIDERS, IdleTimer, StreamOutput from .hls import async_setup_hls +from .recorder import RecorderOutput _LOGGER = logging.getLogger(__name__) STREAM_SOURCE_RE = re.compile("//.*:.*@") -def redact_credentials(data): +def redact_credentials(data: str) -> str: """Redact credentials from string data.""" return STREAM_SOURCE_RE.sub("//****:****@", data) -def create_stream(hass, stream_source, options=None): +def create_stream( + hass: HomeAssistant, stream_source: str, options: dict[str, str] +) -> Stream: """Create a stream with the specified identfier based on the source url. The stream_source is typically an rtsp url and options are passed into @@ -60,9 +66,6 @@ def create_stream(hass, stream_source, options=None): if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") - if options is None: - options = {} - # For RTSP streams, prefer TCP if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": options = { @@ -76,7 +79,7 @@ def create_stream(hass, stream_source, options=None): return stream -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up stream.""" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) @@ -98,7 +101,7 @@ async def async_setup(hass, config): async_setup_recorder(hass) @callback - def shutdown(event): + def shutdown(event: Event) -> None: """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: stream.keepalive = False @@ -113,41 +116,43 @@ async def async_setup(hass, config): class Stream: """Represents a single stream.""" - def __init__(self, hass, source, options=None): + def __init__( + self, hass: HomeAssistant, source: str, options: dict[str, str] + ) -> None: """Initialize a stream.""" self.hass = hass self.source = source self.options = options self.keepalive = False - self.access_token = None - self._thread = None + self.access_token: str | None = None + self._thread: threading.Thread | None = None self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False - if self.options is None: - self.options = {} - def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" if fmt not in self._outputs: raise ValueError(f"Stream is not configured for format '{fmt}'") if not self.access_token: self.access_token = secrets.token_hex() - return self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(self.access_token) + endpoint_fmt: str = self.hass.data[DOMAIN][ATTR_ENDPOINTS][fmt] + return endpoint_fmt.format(self.access_token) - def outputs(self): + def outputs(self) -> Mapping[str, StreamOutput]: """Return a copy of the stream outputs.""" # A copy is returned so the caller can iterate through the outputs # without concern about self._outputs being modified from another thread. return MappingProxyType(self._outputs.copy()) - def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): + def add_provider( + self, fmt: str, timeout: int = OUTPUT_IDLE_TIMEOUT + ) -> StreamOutput: """Add provider output stream.""" if not self._outputs.get(fmt): @callback - def idle_callback(): + def idle_callback() -> None: if ( not self.keepalive or fmt == RECORDER_PROVIDER ) and fmt in self._outputs: @@ -160,7 +165,7 @@ class Stream: self._outputs[fmt] = provider return self._outputs[fmt] - def remove_provider(self, provider): + def remove_provider(self, provider: StreamOutput) -> None: """Remove provider output stream.""" if provider.name in self._outputs: self._outputs[provider.name].cleanup() @@ -169,12 +174,12 @@ class Stream: if not self._outputs: self.stop() - def check_idle(self): + def check_idle(self) -> None: """Reset access token if all providers are idle.""" if all(p.idle for p in self._outputs.values()): self.access_token = None - def start(self): + def start(self) -> None: """Start a stream.""" if self._thread is None or not self._thread.is_alive(): if self._thread is not None: @@ -189,14 +194,14 @@ class Stream: self._thread.start() _LOGGER.info("Started stream: %s", redact_credentials(str(self.source))) - def update_source(self, new_source): + def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" _LOGGER.debug("Updating stream source %s", new_source) self.source = new_source self._fast_restart_once = True self._thread_quit.set() - def _run_worker(self): + def _run_worker(self) -> None: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel @@ -229,17 +234,17 @@ class Stream: ) self._worker_finished() - def _worker_finished(self): + def _worker_finished(self) -> None: """Schedule cleanup of all outputs.""" @callback - def remove_outputs(): + def remove_outputs() -> None: for provider in self.outputs().values(): self.remove_provider(provider) self.hass.loop.call_soon_threadsafe(remove_outputs) - def stop(self): + def stop(self) -> None: """Remove outputs and access token.""" self._outputs = {} self.access_token = None @@ -247,7 +252,7 @@ class Stream: if not self.keepalive: self._stop() - def _stop(self): + def _stop(self) -> None: """Stop worker thread.""" if self._thread is not None: self._thread_quit.set() @@ -255,7 +260,9 @@ class Stream: self._thread = None _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source))) - async def async_record(self, video_path, duration=30, lookback=5): + async def async_record( + self, video_path: str, duration: int = 30, lookback: int = 5 + ) -> None: """Make a .mp4 recording from a provided stream.""" # Check for file access @@ -265,10 +272,13 @@ class Stream: # Add recorder recorder = self.outputs().get(RECORDER_PROVIDER) if recorder: + assert isinstance(recorder, RecorderOutput) raise HomeAssistantError( f"Stream already recording to {recorder.video_path}!" ) - recorder = self.add_provider(RECORDER_PROVIDER, timeout=duration) + recorder = cast( + RecorderOutput, self.add_provider(RECORDER_PROVIDER, timeout=duration) + ) recorder.video_path = video_path self.start() diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 136c3c1dbfa..5f8bb736761 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -4,18 +4,21 @@ from __future__ import annotations import asyncio from collections import deque import datetime -from typing import Callable +from typing import TYPE_CHECKING from aiohttp import web import attr -from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant, callback +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry from .const import ATTR_STREAMS, DOMAIN +if TYPE_CHECKING: + from . import Stream + PROVIDERS = Registry() @@ -59,34 +62,34 @@ class IdleTimer: """ def __init__( - self, hass: HomeAssistant, timeout: int, idle_callback: Callable[[], None] + self, hass: HomeAssistant, timeout: int, idle_callback: CALLBACK_TYPE ) -> None: """Initialize IdleTimer.""" self._hass = hass self._timeout = timeout self._callback = idle_callback - self._unsub = None + self._unsub: CALLBACK_TYPE | None = None self.idle = False - def start(self): + def start(self) -> None: """Start the idle timer if not already started.""" self.idle = False if self._unsub is None: self._unsub = async_call_later(self._hass, self._timeout, self.fire) - def awake(self): + def awake(self) -> None: """Keep the idle time alive by resetting the timeout.""" self.idle = False # Reset idle timeout self.clear() self._unsub = async_call_later(self._hass, self._timeout, self.fire) - def clear(self): + def clear(self) -> None: """Clear and disable the timer if it has not already fired.""" if self._unsub is not None: self._unsub() - def fire(self, _now=None): + def fire(self, _now: datetime.datetime) -> None: """Invoke the idle timeout callback, called when the alarm fires.""" self.idle = True self._unsub = None @@ -97,7 +100,10 @@ class StreamOutput: """Represents a stream output.""" def __init__( - self, hass: HomeAssistant, idle_timer: IdleTimer, deque_maxlen: int = None + self, + hass: HomeAssistant, + idle_timer: IdleTimer, + deque_maxlen: int | None = None, ) -> None: """Initialize a stream output.""" self._hass = hass @@ -172,7 +178,7 @@ class StreamOutput: self._event.set() self._event.clear() - def cleanup(self): + def cleanup(self) -> None: """Handle cleanup.""" self._event.set() self.idle_timer.clear() @@ -190,7 +196,9 @@ class StreamView(HomeAssistantView): requires_auth = False platform = None - async def get(self, request, token, sequence=None): + async def get( + self, request: web.Request, token: str, sequence: str = "" + ) -> web.StreamResponse: """Start a GET request.""" hass = request.app["hass"] @@ -207,6 +215,8 @@ class StreamView(HomeAssistantView): return await self.handle(request, stream, sequence) - async def handle(self, request, stream, sequence): + async def handle( + self, request: web.Request, stream: Stream, sequence: str + ) -> web.StreamResponse: """Handle the stream request.""" raise NotImplementedError() diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 0b0cd4ac3b2..d7167e0b7de 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,7 +1,11 @@ """Provide functionality to stream HLS.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + from aiohttp import web -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from .const import ( EXT_X_START, @@ -10,12 +14,15 @@ from .const import ( MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS, ) -from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView +from .core import PROVIDERS, IdleTimer, StreamOutput, StreamView from .fmp4utils import get_codec_string +if TYPE_CHECKING: + from . import Stream + @callback -def async_setup_hls(hass): +def async_setup_hls(hass: HomeAssistant) -> str: """Set up api endpoints.""" hass.http.register_view(HlsPlaylistView()) hass.http.register_view(HlsSegmentView()) @@ -32,12 +39,13 @@ class HlsMasterPlaylistView(StreamView): cors_allowed = True @staticmethod - def render(track): + def render(track: StreamOutput) -> str: """Render M3U8 file.""" # Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work # Calculate file size / duration and use a small multiplier to account for variation # hls spec already allows for 25% variation - segment = track.get_segment(track.sequences[-2]) + if not (segment := track.get_segment(track.sequences[-2])): + return "" bandwidth = round( (len(segment.init) + sum(len(part.data) for part in segment.parts)) * 8 @@ -52,7 +60,9 @@ class HlsMasterPlaylistView(StreamView): ] return "\n".join(lines) + "\n" - async def handle(self, request, stream, sequence): + async def handle( + self, request: web.Request, stream: Stream, sequence: str + ) -> web.Response: """Return m3u8 playlist.""" track = stream.add_provider(HLS_PROVIDER) stream.start() @@ -73,7 +83,7 @@ class HlsPlaylistView(StreamView): cors_allowed = True @staticmethod - def render(track): + def render(track: StreamOutput) -> str: """Render playlist.""" # NUM_PLAYLIST_SEGMENTS+1 because most recent is probably not yet complete segments = list(track.get_segments())[-(NUM_PLAYLIST_SEGMENTS + 1) :] @@ -130,7 +140,9 @@ class HlsPlaylistView(StreamView): return "\n".join(playlist) + "\n" - async def handle(self, request, stream, sequence): + async def handle( + self, request: web.Request, stream: Stream, sequence: str + ) -> web.Response: """Return m3u8 playlist.""" track = stream.add_provider(HLS_PROVIDER) stream.start() @@ -154,7 +166,9 @@ class HlsInitView(StreamView): name = "api:stream:hls:init" cors_allowed = True - async def handle(self, request, stream, sequence): + async def handle( + self, request: web.Request, stream: Stream, sequence: str + ) -> web.Response: """Return init.mp4.""" track = stream.add_provider(HLS_PROVIDER) if not (segments := track.get_segments()): @@ -170,7 +184,9 @@ class HlsSegmentView(StreamView): name = "api:stream:hls:segment" cors_allowed = True - async def handle(self, request, stream, sequence): + async def handle( + self, request: web.Request, stream: Stream, sequence: str + ) -> web.Response: """Return fmp4 segment.""" track = stream.add_provider(HLS_PROVIDER) track.idle_timer.awake() diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 8e21777fa0b..99276d9763c 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -23,11 +23,11 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_recorder(hass): +def async_setup_recorder(hass: HomeAssistant) -> None: """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: deque[Segment]): +def recorder_save_worker(file_out: str, segments: deque[Segment]) -> None: """Handle saving stream.""" if not segments: @@ -121,7 +121,7 @@ class RecorderOutput(StreamOutput): def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: """Initialize recorder output.""" super().__init__(hass, idle_timer) - self.video_path = None + self.video_path: str @property def name(self) -> str: @@ -132,7 +132,7 @@ class RecorderOutput(StreamOutput): """Prepend segments to existing list.""" self._segments.extendleft(reversed(segments)) - def cleanup(self): + def cleanup(self) -> None: """Write recording and clean up.""" _LOGGER.debug("Starting recorder worker thread") thread = threading.Thread( diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index cca981e5db3..3023b8cd85c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -7,7 +7,7 @@ from fractions import Fraction from io import BytesIO import logging from threading import Event -from typing import Callable, cast +from typing import Any, Callable, cast import av @@ -45,9 +45,9 @@ class SegmentBuffer: self._memory_file: BytesIO = cast(BytesIO, None) self._av_output: av.container.OutputContainer = None self._input_video_stream: av.video.VideoStream = None - self._input_audio_stream = None # av.audio.AudioStream | None + self._input_audio_stream: Any | None = None # av.audio.AudioStream | None self._output_video_stream: av.video.VideoStream = None - self._output_audio_stream = None # av.audio.AudioStream | None + self._output_audio_stream: Any | None = None # av.audio.AudioStream | None self._segment: Segment | None = None self._segment_last_write_pos: int = cast(int, None) self._part_start_dts: int = cast(int, None) @@ -82,7 +82,7 @@ class SegmentBuffer: def set_streams( self, video_stream: av.video.VideoStream, - audio_stream, + audio_stream: Any, # no type hint for audio_stream until https://github.com/PyAV-Org/PyAV/pull/775 is merged ) -> None: """Initialize output buffer with streams from container.""" @@ -206,7 +206,10 @@ class SegmentBuffer: def stream_worker( # noqa: C901 - source: str, options: dict, segment_buffer: SegmentBuffer, quit_event: Event + source: str, + options: dict[str, str], + segment_buffer: SegmentBuffer, + quit_event: Event, ) -> None: """Handle consuming streams.""" @@ -259,7 +262,7 @@ def stream_worker( # noqa: C901 found_audio = False try: container_packets = container.demux((video_stream, audio_stream)) - first_packet = None + first_packet: av.Packet | None = None # Get to first video keyframe while first_packet is None: packet = next(container_packets) @@ -315,7 +318,6 @@ def stream_worker( # noqa: C901 _LOGGER.warning( "Audio stream not found" ) # Some streams declare an audio stream and never send any packets - audio_stream = None except (av.AVError, StopIteration) as ex: _LOGGER.error( diff --git a/mypy.ini b/mypy.ini index 775b0653fe4..b21c71d172c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -726,6 +726,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.stream.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sun.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 37c499b6bd0..89c07083b17 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -107,7 +107,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # Request stream stream.add_provider(HLS_PROVIDER) @@ -148,7 +148,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # Request stream stream.add_provider(HLS_PROVIDER) @@ -190,7 +190,7 @@ async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): # Setup demo HLS track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # Request stream stream.add_provider(HLS_PROVIDER) @@ -212,7 +212,7 @@ async def test_stream_keepalive(hass): # Setup demo HLS track source = "test_stream_keepalive_source" - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 @@ -247,7 +247,7 @@ async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream): """Test rendering the hls playlist with no output segments.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, STREAM_SOURCE) + stream = create_stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) @@ -261,7 +261,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): """Test rendering the hls playlist with 1 and 2 output segments.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, STREAM_SOURCE) + stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -295,7 +295,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): """Test rendering the hls playlist with more segments than the segment deque can hold.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, STREAM_SOURCE) + stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -347,7 +347,7 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s """Test a discontinuity across segments in the stream with 3 segments.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, STREAM_SOURCE) + stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -389,7 +389,7 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy """Test a discontinuity with more segments than the segment deque can hold.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, STREAM_SOURCE) + stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 07e1464f31a..72c6dfa197f 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -34,7 +34,7 @@ async def test_record_stream(hass, hass_client, record_worker_sync): # Setup demo track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") @@ -56,7 +56,7 @@ async def test_record_lookback( await async_setup_component(hass, "stream", {"stream": {}}) source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # Start an HLS feed to enable lookback stream.add_provider(HLS_PROVIDER) @@ -85,7 +85,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync): # Setup demo track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") recorder = stream.add_provider(RECORDER_PROVIDER) @@ -111,7 +111,7 @@ async def test_record_path_not_allowed(hass, hass_client): # Setup demo track source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) with patch.object( hass.config, "is_allowed_path", return_value=False ), pytest.raises(HomeAssistantError): @@ -203,7 +203,7 @@ async def test_record_stream_audio( source = generate_h264_video( container_format="mov", audio_codec=a_codec ) # mov can store PCM - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") recorder = stream.add_provider(RECORDER_PROVIDER) @@ -234,7 +234,7 @@ async def test_record_stream_audio( async def test_recorder_log(hass, caplog): """Test starting a stream to record logs the url without username and password.""" await async_setup_component(hass, "stream", {"stream": {}}) - stream = create_stream(hass, "https://abcd:efgh@foo.bar") + stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") assert "https://abcd:efgh@foo.bar" not in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 74a4fa0e553..793038c6770 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -220,7 +220,7 @@ class MockFlushPart: async def async_decode_stream(hass, packets, py_av=None): """Start a stream worker that decodes incoming stream packets into output segments.""" - stream = Stream(hass, STREAM_SOURCE) + stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) if not py_av: @@ -244,7 +244,7 @@ async def async_decode_stream(hass, packets, py_av=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" - stream = Stream(hass, STREAM_SOURCE) + stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") @@ -565,7 +565,7 @@ async def test_stream_stopped_while_decoding(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE) + stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) py_av = MockPyAv() @@ -592,7 +592,7 @@ async def test_update_stream_source(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE) + stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) # Note that keepalive is not set here. The stream is "restarted" even though # it is not stopping due to failure. @@ -636,7 +636,7 @@ async def test_update_stream_source(hass): async def test_worker_log(hass, caplog): """Test that the worker logs the url without username and password.""" - stream = Stream(hass, "https://abcd:efgh@foo.bar") + stream = Stream(hass, "https://abcd:efgh@foo.bar", {}) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: av_open.side_effect = av.error.InvalidDataError(-2, "error") @@ -654,7 +654,7 @@ async def test_durations(hass, record_worker_sync): await async_setup_component(hass, "stream", {"stream": {}}) source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -693,7 +693,7 @@ async def test_has_keyframe(hass, record_worker_sync): await async_setup_component(hass, "stream", {"stream": {}}) source = generate_h264_video() - stream = create_stream(hass, source) + stream = create_stream(hass, source, {}) # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): From c8755cd896b057dc0ec9789eb4f65c1dc21aeac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 14 Jun 2021 18:01:18 +0200 Subject: [PATCH 355/750] Migrate the name for the hassio user (#51771) --- homeassistant/components/hassio/__init__.py | 6 +++++- tests/auth/test_init.py | 18 ++++++++++++++++++ tests/components/hassio/test_init.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 419e6865b69..1feb34cd173 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -400,8 +400,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if not user.is_admin: await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN]) + # Migrate old name + if user.name == "Hass.io": + await hass.auth.async_update_user(user, name="Supervisor") + if refresh_token is None: - user = await hass.auth.async_create_system_user("Hass.io", [GROUP_ID_ADMIN]) + user = await hass.auth.async_create_system_user("Supervisor", [GROUP_ID_ADMIN]) refresh_token = await hass.auth.async_create_refresh_token(user) data["hassio_user"] = user.id await store.async_save(data) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 255fcac7694..0128c9794f3 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1001,3 +1001,21 @@ async def test_new_users(mock_hass): ) ) assert user_cred.is_admin + + +async def test_rename_does_not_change_refresh_token(mock_hass): + """Test that we can rename without changing refresh token.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + await manager.async_create_refresh_token(user, CLIENT_ID) + + assert len(list(user.refresh_tokens.values())) == 1 + token_before = list(user.refresh_tokens.values())[0] + + await manager.async_update_user(user, name="new name") + assert user.name == "new name" + + assert len(list(user.refresh_tokens.values())) == 1 + token_after = list(user.refresh_tokens.values())[0] + + assert token_before == token_after diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 7e9d7cd91c8..8377e5287d0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -179,6 +179,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag assert hassio_user.system_generated assert len(hassio_user.groups) == 1 assert hassio_user.groups[0].id == GROUP_ID_ADMIN + assert hassio_user.name == "Supervisor" for token in hassio_user.refresh_tokens.values(): if token.token == refresh_token: break @@ -206,6 +207,25 @@ async def test_setup_adds_admin_group_to_user(hass, aioclient_mock, hass_storage assert user.is_admin +async def test_setup_migrate_user_name(hass, aioclient_mock, hass_storage): + """Test setup with migrating the user name.""" + # Create user with old name + user = await hass.auth.async_create_system_user("Hass.io") + await hass.auth.async_create_refresh_token(user) + + hass_storage[STORAGE_KEY] = { + "data": {"hassio_user": user.id}, + "key": STORAGE_KEY, + "version": 1, + } + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) + assert result + + assert user.name == "Supervisor" + + async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage): """Test setup with API push default data.""" user = await hass.auth.async_create_system_user("Hass.io test") From 32409a2c938fddd6e64d9bc4c95a9a2690869172 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Jun 2021 18:04:46 +0200 Subject: [PATCH 356/750] Define HumidifierEntity entity attributes as class variables (#51841) --- homeassistant/components/demo/humidifier.py | 84 ++++++------------- .../components/humidifier/__init__.py | 16 ++-- 2 files changed, 36 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index 35eb6e18537..7ee5c0fc6ef 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,4 +1,6 @@ """Demo platform that offers a fake humidifier device.""" +from __future__ import annotations + from homeassistant.components.humidifier import HumidifierEntity from homeassistant.components.humidifier.const import ( DEVICE_CLASS_DEHUMIDIFIER, @@ -43,82 +45,46 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoHumidifier(HumidifierEntity): """Representation of a demo humidifier device.""" + _attr_should_poll = False + def __init__( self, - name, - mode, - target_humidity, - available_modes=None, - is_on=True, - device_class=None, - ): + name: str, + mode: str | None, + target_humidity: int, + available_modes: list[str] | None = None, + is_on: bool = True, + device_class: str | None = None, + ) -> None: """Initialize the humidifier device.""" - self._name = name - self._state = is_on - self._support_flags = SUPPORT_FLAGS + self._attr_name = name + self._attr_is_on = is_on + self._attr_supported_features = SUPPORT_FLAGS if mode is not None: - self._support_flags = self._support_flags | SUPPORT_MODES - self._target_humidity = target_humidity - self._mode = mode - self._available_modes = available_modes - self._device_class = device_class - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def name(self): - """Return the name of the humidity device.""" - return self._name - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - return self._target_humidity - - @property - def mode(self): - """Return current mode.""" - return self._mode - - @property - def available_modes(self): - """Return available modes.""" - return self._available_modes - - @property - def is_on(self): - """Return true if the humidifier is on.""" - return self._state - - @property - def device_class(self): - """Return the device class of the humidifier.""" - return self._device_class + self._attr_supported_features = ( + self._attr_supported_features | SUPPORT_MODES + ) + self._attr_target_humidity = target_humidity + self._attr_mode = mode + self._attr_available_modes = available_modes + self._attr_device_class = device_class async def async_turn_on(self, **kwargs): """Turn the device on.""" - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_set_humidity(self, humidity): """Set new humidity level.""" - self._target_humidity = humidity + self._attr_target_humidity = humidity self.async_write_ha_state() async def async_set_mode(self, mode): """Update mode.""" - self._mode = mode + self._attr_mode = mode self.async_write_ha_state() diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 9500b74aba6..31bff2fe3f4 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -102,6 +102,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class HumidifierEntity(ToggleEntity): """Base class for humidifier entities.""" + _attr_available_modes: list[str] | None + _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY + _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY + _attr_mode: str | None + _attr_target_humidity: int | None = None + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" @@ -134,7 +140,7 @@ class HumidifierEntity(ToggleEntity): @property def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" - return None + return self._attr_target_humidity @property def mode(self) -> str | None: @@ -142,7 +148,7 @@ class HumidifierEntity(ToggleEntity): Requires SUPPORT_MODES. """ - raise NotImplementedError + return self._attr_mode @property def available_modes(self) -> list[str] | None: @@ -150,7 +156,7 @@ class HumidifierEntity(ToggleEntity): Requires SUPPORT_MODES. """ - raise NotImplementedError + return self._attr_available_modes def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" @@ -171,9 +177,9 @@ class HumidifierEntity(ToggleEntity): @property def min_humidity(self) -> int: """Return the minimum humidity.""" - return DEFAULT_MIN_HUMIDITY + return self._attr_min_humidity @property def max_humidity(self) -> int: """Return the maximum humidity.""" - return DEFAULT_MAX_HUMIDITY + return self._attr_max_humidity From 347ef9cb4c7ed6c540944dc5f2f34898239a713d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Jun 2021 18:05:01 +0200 Subject: [PATCH 357/750] Define NumberEntity entity attributes as class variables (#51842) --- homeassistant/components/demo/number.py | 87 ++++++--------------- homeassistant/components/number/__init__.py | 18 +++-- 2 files changed, 38 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index f3fd815f621..a6842d2ca43 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,4 +1,6 @@ """Demo platform that offers a fake Number entity.""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.number import NumberEntity @@ -40,26 +42,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoNumber(NumberEntity): """Representation of a demo Number entity.""" + _attr_should_poll = False + def __init__( self, - unique_id, - name, - state, - icon, - assumed, - min_value=None, - max_value=None, + unique_id: str, + name: str, + state: float, + icon: str, + assumed: bool, + min_value: float | None = None, + max_value: float | None = None, step=None, - ): + ) -> None: """Initialize the Demo Number entity.""" - self._unique_id = unique_id - self._name = name or DEVICE_DEFAULT_NAME - self._state = state - self._icon = icon - self._assumed = assumed - self._min_value = min_value - self._max_value = max_value - self._step = step + self._attr_assumed_state = assumed + self._attr_icon = icon + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_unique_id = unique_id + self._attr_value = state + + if min_value is not None: + self._attr_min_value = min_value + if max_value is not None: + self._attr_max_value = max_value + if step is not None: + self._attr_step = step @property def device_info(self): @@ -72,51 +80,6 @@ class DemoNumber(NumberEntity): "name": self.name, } - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """No polling needed for a demo Number entity.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def assumed_state(self): - """Return if the state is based on assumptions.""" - return self._assumed - - @property - def value(self): - """Return the current value.""" - return self._state - - @property - def min_value(self): - """Return the minimum value.""" - return self._min_value or super().min_value - - @property - def max_value(self): - """Return the maximum value.""" - return self._max_value or super().max_value - - @property - def step(self): - """Return the value step.""" - return self._step or super().step - async def async_set_value(self, value): """Update the current value.""" num_value = float(value) @@ -126,5 +89,5 @@ class DemoNumber(NumberEntity): f"Invalid value for {self.entity_id}: {value} (range {self.min_value} - {self.max_value})" ) - self._state = num_value + self._attr_value = num_value self.async_write_ha_state() diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 046895ac29c..897186dce7b 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,10 +1,9 @@ """Component to allow numeric input for platforms.""" from __future__ import annotations -from abc import abstractmethod from datetime import timedelta import logging -from typing import Any +from typing import Any, final import voluptuous as vol @@ -68,6 +67,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class NumberEntity(Entity): """Representation of a Number entity.""" + _attr_max_value: float = DEFAULT_MAX_VALUE + _attr_min_value: float = DEFAULT_MIN_VALUE + _attr_state: None = None + _attr_step: float + _attr_value: float + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" @@ -80,16 +85,18 @@ class NumberEntity(Entity): @property def min_value(self) -> float: """Return the minimum value.""" - return DEFAULT_MIN_VALUE + return self._attr_min_value @property def max_value(self) -> float: """Return the maximum value.""" - return DEFAULT_MAX_VALUE + return self._attr_max_value @property def step(self) -> float: """Return the increment/decrement step.""" + if hasattr(self, "_attr_step"): + return self._attr_step step = DEFAULT_STEP value_range = abs(self.max_value - self.min_value) if value_range != 0: @@ -98,14 +105,15 @@ class NumberEntity(Entity): return step @property + @final def state(self) -> float: """Return the entity state.""" return self.value @property - @abstractmethod def value(self) -> float: """Return the entity value to represent the entity state.""" + return self._attr_value def set_value(self, value: float) -> None: """Set new value.""" From 0d40ba463ead067f300b9f68ec278e232e125767 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 14 Jun 2021 13:31:44 -0400 Subject: [PATCH 358/750] Create zwave_js node status sensor when the node is added (#51850) * Create node status sensor when the node is added * Handle race condition * reduce repeat code --- homeassistant/components/zwave_js/__init__.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2878580e30c..fee4da743c8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -179,19 +179,6 @@ async def async_setup_entry( # noqa: C901 if disc_info.assumed_state: value_updates_disc_info.append(disc_info) - # We need to set up the sensor platform if it hasn't already been setup in - # order to create the node status sensor - if SENSOR_DOMAIN not in platform_setup_tasks: - platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) - ) - await platform_setup_tasks[SENSOR_DOMAIN] - - # Create a node status sensor for each device - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) - # add listener for value updated events if necessary if value_updates_disc_info: unsubscribe_callbacks.append( @@ -220,6 +207,25 @@ async def async_setup_entry( # noqa: C901 async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" + platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] + + # We need to set up the sensor platform if it hasn't already been setup in + # order to create the node status sensor + if SENSOR_DOMAIN not in platform_setup_tasks: + platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) + ) + + # This guard ensures that concurrent runs of this function all await the + # platform setup task + if not platform_setup_tasks[SENSOR_DOMAIN].done(): + await platform_setup_tasks[SENSOR_DOMAIN] + + # Create a node status sensor for each device + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node + ) + # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: From e9297744813170b49538158e7ee8b5b7e0e4a5e7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 14 Jun 2021 13:09:27 -0500 Subject: [PATCH 359/750] Add warning during playback if Plex token missing (#51853) --- homeassistant/components/plex/media_player.py | 4 ++++ homeassistant/components/plex/server.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 650ed2c89b0..f9c40f1edc3 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -492,6 +492,10 @@ class PlexMediaPlayer(MediaPlayerEntity): "Client is not currently accepting playback controls: %s", self.name ) return + if not self.plex_server.has_token: + _LOGGER.warning( + "Plex integration configured without a token, playback may fail" + ) src = json.loads(media_id) if isinstance(src, int): diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 4dcdda044eb..dc05a727fee 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -537,6 +537,11 @@ class PlexServer: """Return the plexapi PlexServer instance.""" return self._plex_server + @property + def has_token(self): + """Return if a token is used to connect to this Plex server.""" + return self._token is not None + @property def accounts(self): """Return accounts associated with the Plex server.""" From 7329dc4f6bf02d746f2f5660b3065c3a3c3f3452 Mon Sep 17 00:00:00 2001 From: yllar Date: Mon, 14 Jun 2021 21:10:21 +0300 Subject: [PATCH 360/750] Add missing languages to Microsoft TTS (#51774) --- homeassistant/components/microsoft/tts.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 1e1c088b351..1c86d67ab47 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -20,8 +20,10 @@ _LOGGER = logging.getLogger(__name__) SUPPORTED_LANGUAGES = [ "ar-eg", "ar-sa", + "bg-bg", "ca-es", "cs-cz", + "cy-gb", "da-dk", "de-at", "de-ch", @@ -30,23 +32,42 @@ SUPPORTED_LANGUAGES = [ "en-au", "en-ca", "en-gb", + "en-hk", "en-ie", "en-in", + "en-nz", + "en-ph", + "en-sg", "en-us", + "en-za", + "es-ar", + "es-co", "es-es", "es-mx", + "es-us", + "et-ee", "fi-fi", + "fr-be", "fr-ca", "fr-ch", "fr-fr", + "ga-ie", + "gu-in", "he-il", "hi-in", + "hr-hr", "hu-hu", "id-id", "it-it", "ja-jp", "ko-kr", + "lt-lt", + "lv-lv", + "mr-in", + "ms-my", + "mt-mt", "nb-no", + "nl-be", "nl-nl", "pl-pl", "pt-br", @@ -56,8 +77,14 @@ SUPPORTED_LANGUAGES = [ "sk-sk", "sl-si", "sv-se", + "sw-ke", + "ta-in", + "te-in", "th-th", "tr-tr", + "uk-ua", + "ur-pk", + "vi-vn", "zh-cn", "zh-hk", "zh-tw", From b1fa01e4bc124d85db069a7f1e26954e47d41c7b Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Mon, 14 Jun 2021 14:04:02 -0500 Subject: [PATCH 361/750] Cleanup of code reviews from initial modern forms (#51794) --- .../components/modern_forms/__init__.py | 21 ++------ .../components/modern_forms/const.py | 14 ----- homeassistant/components/modern_forms/fan.py | 42 +++++---------- .../components/modern_forms/manifest.json | 1 - .../components/modern_forms/services.yaml | 3 -- .../components/modern_forms/strings.json | 4 -- .../modern_forms/test_config_flow.py | 51 +++++++++++++++---- tests/components/modern_forms/test_fan.py | 2 +- tests/components/modern_forms/test_init.py | 8 --- 9 files changed, 58 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 3d5a7c50315..4e80c85cd52 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -15,7 +15,6 @@ from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -38,24 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create Modern Forms instance for this entry coordinator = ModernFormsDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - if entry.unique_id is None: - hass.config_entries.async_update_entry( - entry, unique_id=coordinator.data.info.mac_address - ) - # Set up all platforms for this device/entry. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -106,7 +94,7 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt host: str, ) -> None: """Initialize global Modern Forms data updater.""" - self.modernforms = ModernFormsDevice( + self.modern_forms = ModernFormsDevice( host, session=async_get_clientsession(hass) ) @@ -125,7 +113,7 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt async def _async_update_data(self) -> ModernFormsDevice: """Fetch data from Modern Forms.""" try: - return await self.modernforms.update( + return await self.modern_forms.update( full_update=not self.last_update_success ) except ModernFormsError as error: @@ -152,7 +140,6 @@ class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator self._entry_id = entry_id self._attr_icon = icon self._attr_name = name - self._unsub_dispatcher = None @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/modern_forms/const.py b/homeassistant/components/modern_forms/const.py index 60791d97e64..b151a637d75 100644 --- a/homeassistant/components/modern_forms/const.py +++ b/homeassistant/components/modern_forms/const.py @@ -4,25 +4,11 @@ DOMAIN = "modern_forms" ATTR_IDENTIFIERS = "identifiers" ATTR_MANUFACTURER = "manufacturer" -ATTR_MODEL = "model" -ATTR_OWNER = "owner" -ATTR_IDENTITY = "identity" -ATTR_MCU_FIRMWARE_VERSION = "mcu_firmware_version" -ATTR_FIRMWARE_VERSION = "firmware_version" -SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}" -SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}" -SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}" - -CONF_ON_UNLOAD = "ON_UNLOAD" - -OPT_BRIGHTNESS = "brightness" OPT_ON = "on" OPT_SPEED = "speed" # Services -SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer" -SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer" SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer" SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer" diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 86c68df6eee..2668b26857b 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -1,16 +1,15 @@ """Support for Modern Forms Fan Fans.""" from __future__ import annotations -from functools import partial -from typing import Any, Callable +from typing import Any from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON import voluptuous as vol from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( int_states_in_range, @@ -35,7 +34,9 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Modern Forms platform from config entry.""" @@ -61,11 +62,9 @@ async def async_setup_entry( "async_clear_fan_sleep_timer", ) - update_func = partial( - async_update_fan, config_entry, coordinator, {}, async_add_entities + async_add_entities( + [ModernFormsFanEntity(entry_id=config_entry.entry_id, coordinator=coordinator)] ) - coordinator.async_add_listener(update_func) - update_func() class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): @@ -82,7 +81,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): coordinator=coordinator, name=f"{coordinator.data.info.device_name} Fan", ) - self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_fan" + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" @property def supported_features(self) -> int: @@ -117,7 +116,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): @modernforms_exception_handler async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - await self.coordinator.modernforms.fan(direction=direction) + await self.coordinator.modern_forms.fan(direction=direction) @modernforms_exception_handler async def async_set_percentage(self, percentage: int) -> None: @@ -142,12 +141,12 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): data[OPT_SPEED] = round( percentage_to_ranged_value(self.SPEED_RANGE, percentage) ) - await self.coordinator.modernforms.fan(**data) + await self.coordinator.modern_forms.fan(**data) @modernforms_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" - await self.coordinator.modernforms.fan(on=FAN_POWER_OFF) + await self.coordinator.modern_forms.fan(on=FAN_POWER_OFF) @modernforms_exception_handler async def async_set_fan_sleep_timer( @@ -155,26 +154,11 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): sleep_time: int, ) -> None: """Set a Modern Forms light sleep timer.""" - await self.coordinator.modernforms.fan(sleep=sleep_time * 60) + await self.coordinator.modern_forms.fan(sleep=sleep_time * 60) @modernforms_exception_handler async def async_clear_fan_sleep_timer( self, ) -> None: """Clear a Modern Forms fan sleep timer.""" - await self.coordinator.modernforms.fan(sleep=CLEAR_TIMER) - - -@callback -def async_update_fan( - entry: ConfigEntry, - coordinator: ModernFormsDataUpdateCoordinator, - current: dict[str, ModernFormsFanEntity], - async_add_entities, -) -> None: - """Update Modern Forms Fan info.""" - if not current: - current[entry.entry_id] = ModernFormsFanEntity( - entry_id=entry.entry_id, coordinator=coordinator - ) - async_add_entities([current[entry.entry_id]]) + await self.coordinator.modern_forms.fan(sleep=CLEAR_TIMER) diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json index 11d50e7353b..c2da0239fbe 100644 --- a/homeassistant/components/modern_forms/manifest.json +++ b/homeassistant/components/modern_forms/manifest.json @@ -9,7 +9,6 @@ "zeroconf": [ {"type":"_easylink._tcp.local.", "name":"wac*"} ], - "dependencies": [], "codeowners": [ "@wonderslug" ], diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml index 6eeb423c36f..b90fec11bf1 100644 --- a/homeassistant/components/modern_forms/services.yaml +++ b/homeassistant/components/modern_forms/services.yaml @@ -15,10 +15,7 @@ set_fan_sleep_timer: number: min: 1 max: 1440 - step: 1 unit_of_measurement: minutes - mode: slider - clear_fan_sleep_timer: name: Clear fan sleep timer description: Clear the sleep timer on a Modern Forms fan. diff --git a/homeassistant/components/modern_forms/strings.json b/homeassistant/components/modern_forms/strings.json index 097217692ae..fc30709960b 100644 --- a/homeassistant/components/modern_forms/strings.json +++ b/homeassistant/components/modern_forms/strings.json @@ -1,5 +1,4 @@ { - "title": "Modern Forms", "config": { "flow_title": "{name}", "step": { @@ -9,9 +8,6 @@ "host": "[%key:common::config_flow::data::host%]" } }, - "confirm": { - "description": "[%key:common::config_flow::description::confirm_setup%]" - }, "zeroconf_confirm": { "description": "Do you want to add the Modern Forms fan named `{name}` to Home Assistant?", "title": "Discovered Modern Forms fan device" diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index e7b01bf2fd4..967e9d354d5 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -39,15 +39,20 @@ async def test_full_user_flow_implementation( assert result.get("type") == RESULT_TYPE_FORM assert "flow_id" in result - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} - ) + with patch( + "homeassistant.components.modern_forms.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} + ) - assert result.get("title") == "ModernFormsFan" - assert "data" in result - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_HOST] == "192.168.1.123" - assert result["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" + assert result2.get("title") == "ModernFormsFan" + assert "data" in result2 + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2["data"][CONF_HOST] == "192.168.1.123" + assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" + assert len(mock_setup_entry.mock_calls) == 1 async def test_full_zeroconf_flow_implementation( @@ -166,12 +171,26 @@ async def test_user_device_exists_abort( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock, skip_setup=True) + + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + }, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_HOST: "192.168.1.123"}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + }, ) assert result.get("type") == RESULT_TYPE_ABORT @@ -182,7 +201,17 @@ async def test_zeroconf_with_mac_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow if a Modern Forms device already configured.""" - await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock, skip_setup=True) + + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "AA:BB:CC:DD:EE:FF"}, + }, + ) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/modern_forms/test_fan.py b/tests/components/modern_forms/test_fan.py index bac9a5ed07c..c9b6c66bb62 100644 --- a/tests/components/modern_forms/test_fan.py +++ b/tests/components/modern_forms/test_fan.py @@ -48,7 +48,7 @@ async def test_fan_state( entry = entity_registry.async_get("fan.modernformsfan_fan") assert entry - assert entry.unique_id == "AA:BB:CC:DD:EE:FF_fan" + assert entry.unique_id == "AA:BB:CC:DD:EE:FF" async def test_change_state( diff --git a/tests/components/modern_forms/test_init.py b/tests/components/modern_forms/test_init.py index 6ef7b563918..518355ac18b 100644 --- a/tests/components/modern_forms/test_init.py +++ b/tests/components/modern_forms/test_init.py @@ -39,14 +39,6 @@ async def test_unload_config_entry( assert not hass.data.get(DOMAIN) -async def test_setting_unique_id(hass, aioclient_mock): - """Test we set unique ID if not set yet.""" - entry = await init_integration(hass, aioclient_mock) - - assert hass.data[DOMAIN] - assert entry.unique_id == "AA:BB:CC:DD:EE:FF" - - async def test_fan_only_device(hass, aioclient_mock): """Test we set unique ID if not set yet.""" await init_integration( From f00f2b4ae47f0ffd67b8b29c9175beada192022b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 14 Jun 2021 16:38:35 -0400 Subject: [PATCH 362/750] Add zwave_js ping node service (#51435) * Add zwave_js ping node service * uncomment code * use asyncio.gather --- homeassistant/components/zwave_js/const.py | 2 + homeassistant/components/zwave_js/services.py | 24 +++++++++++ .../components/zwave_js/services.yaml | 7 ++++ tests/components/zwave_js/test_services.py | 41 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index d7717922d10..d73f05c4c47 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -62,4 +62,6 @@ SERVICE_MULTICAST_SET_VALUE = "multicast_set_value" ATTR_BROADCAST = "broadcast" +SERVICE_PING = "ping" + ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 65184abbd08..930f002cf1e 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -1,6 +1,7 @@ """Methods and classes related to executing Z-Wave commands and publishing these to hass.""" from __future__ import annotations +import asyncio import logging from typing import Any @@ -294,6 +295,24 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_PING, + self.async_ping, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + get_nodes_from_service_data, + ), + ), + ) + async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" nodes = service.data[const.ATTR_NODES] @@ -418,3 +437,8 @@ class ZWaveServices: if success is False: raise SetValueFailed("Unable to set value via multicast") + + async def async_ping(self, service: ServiceCall) -> None: + """Ping node(s).""" + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] + await asyncio.gather(*[node.async_ping() for node in nodes]) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 16be02b7f1b..c24fa4694cf 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -210,3 +210,10 @@ multicast_set_value: required: true selector: object: + +ping: + name: Ping a node + description: Forces Z-Wave JS to try to reach a node. This can be used to update the status of the node in Z-Wave JS when you think it doesn't accurately reflect reality, e.g. reviving a failed/dead node or marking the node as asleep. + target: + entity: + integration: zwave_js diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 4f70543d3e2..a7aea70f6a7 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -18,6 +18,7 @@ from homeassistant.components.zwave_js.const import ( DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, SERVICE_MULTICAST_SET_VALUE, + SERVICE_PING, SERVICE_REFRESH_VALUE, SERVICE_SET_CONFIG_PARAMETER, SERVICE_SET_VALUE, @@ -790,3 +791,43 @@ async def test_multicast_set_value( }, blocking=True, ) + + +async def test_ping( + hass, + client, + climate_danfoss_lc_13, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test ping service.""" + client.async_send_command.return_value = {"responded": True} + + # Test successful ping call + await hass.services.async_call( + DOMAIN, + SERVICE_PING, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.ping" + assert args["nodeId"] == climate_danfoss_lc_13.node_id + + client.async_send_command.reset_mock() + + # Test no device or entity raises error + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_PING, + {}, + blocking=True, + ) From 8705168fe6c1da596759399c23de7cd4624fc036 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 14 Jun 2021 16:43:51 -0400 Subject: [PATCH 363/750] Add zwave_js WS API cmds to get node state and version info (#51396) * Add zwave_js view to retrieve a node's state * remove typehints * Make dump views require admin * Add version info to node level dump * Add back typehints * switch from list to dict * switch from dump node view to two WS API commands * switch to snake --- homeassistant/components/zwave_js/api.py | 57 +++++++++- tests/components/zwave_js/test_api.py | 134 ++++++++++++++++++++++- 2 files changed, 189 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index ffd00919941..1b4a44d8436 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -138,6 +138,7 @@ def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_node_status) + websocket_api.async_register_command(hass, websocket_node_state) websocket_api.async_register_command(hass, websocket_node_metadata) websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) @@ -164,6 +165,7 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_update_data_collection_preference ) websocket_api.async_register_command(hass, websocket_data_collection_status) + websocket_api.async_register_command(hass, websocket_version_info) websocket_api.async_register_command(hass, websocket_abort_firmware_update) websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status @@ -253,6 +255,28 @@ async def websocket_node_status( ) +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/node_state", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_node_state( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Get the state data of a Z-Wave JS node.""" + connection.send_result( + msg[ID], + node.data, + ) + + @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_metadata", @@ -1170,6 +1194,8 @@ class DumpView(HomeAssistantView): async def get(self, request: web.Request, config_entry_id: str) -> web.Response: """Dump the state of Z-Wave.""" + if not request["hass_user"].is_admin: + raise Unauthorized() hass = request.app["hass"] if config_entry_id not in hass.data[DOMAIN]: @@ -1188,6 +1214,35 @@ class DumpView(HomeAssistantView): ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/version_info", + vol.Required(ENTRY_ID): str, + }, +) +@websocket_api.async_response +@async_get_entry +async def websocket_version_info( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Get version info from the Z-Wave JS server.""" + version_info = { + "driver_version": client.version.driver_version, + "server_version": client.version.server_version, + "min_schema_version": client.version.min_schema_version, + "max_schema_version": client.version.max_schema_version, + } + connection.send_result( + msg[ID], + version_info, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -1287,7 +1342,7 @@ class FirmwareUploadView(HomeAssistantView): raise web_exceptions.HTTPBadRequest entry = hass.config_entries.async_get_entry(config_entry_id) - client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] + client: Client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] node = client.driver.controller.nodes.get(int(node_id)) if not node: raise web_exceptions.HTTPNotFound diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c498c4201ae..bb34193e2d1 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -119,6 +119,54 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_node_state(hass, multisensor_6, integration, hass_ws_client): + """Test the node_state websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + node = multisensor_6 + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/node_state", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] == node.data + + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/node_state", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/node_state", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_client): """Test the node metadata websocket command.""" entry = integration @@ -1304,6 +1352,57 @@ async def test_dump_view(integration, hass_client): assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}] +async def test_version_info(hass, integration, hass_ws_client, version_state): + """Test the HTTP dump node view.""" + entry = integration + ws_client = await hass_ws_client(hass) + + version_info = { + "driver_version": version_state["driverVersion"], + "server_version": version_state["serverVersion"], + "min_schema_version": 0, + "max_schema_version": 0, + } + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/version_info", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] == version_info + + # Test getting non-existent entry fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/version_info", + ENTRY_ID: "INVALID", + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/version_info", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_firmware_upload_view( hass, multisensor_6, integration, hass_client, firmware_file ): @@ -1348,6 +1447,38 @@ async def test_firmware_upload_view_invalid_payload( assert resp.status == 400 +@pytest.mark.parametrize( + "method, url", + [("get", "/api/zwave_js/dump/{}")], +) +async def test_view_non_admin_user( + integration, hass_client, hass_admin_user, method, url +): + """Test config entry level views for non-admin users.""" + client = await hass_client() + # Verify we require admin user + hass_admin_user.groups = [] + resp = await client.request(method, url.format(integration.entry_id)) + assert resp.status == 401 + + +@pytest.mark.parametrize( + "method, url", + [("post", "/api/zwave_js/firmware/upload/{}/{}")], +) +async def test_node_view_non_admin_user( + multisensor_6, integration, hass_client, hass_admin_user, method, url +): + """Test node level views for non-admin users.""" + client = await hass_client() + # Verify we require admin user + hass_admin_user.groups = [] + resp = await client.request( + method, url.format(integration.entry_id, multisensor_6.node_id) + ) + assert resp.status == 401 + + @pytest.mark.parametrize( "method, url", [ @@ -1363,7 +1494,8 @@ async def test_view_invalid_entry_id(integration, hass_client, method, url): @pytest.mark.parametrize( - "method, url", [("post", "/api/zwave_js/firmware/upload/{}/111")] + "method, url", + [("post", "/api/zwave_js/firmware/upload/{}/111")], ) async def test_view_invalid_node_id(integration, hass_client, method, url): """Test an invalid config entry id parameter.""" From 3a2d50fe23b65601ff2f85357adf7add9a86f23c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 15 Jun 2021 00:05:40 +0200 Subject: [PATCH 364/750] Add Xiaomi Miio EU gateway support (#47955) * Add EU gateway support * add options flow translations * fix options flow * fix missing import * try to fix async_add_executor_job * try to fix async_add_executor_job * fix unload * check for login succes * fix not reloading * use cloud option * fix styling * Return after if Co-authored-by: Nathan Tilley * cleanup * add options flow tests * fix new tests * fix typo in docstring * add missing blank line * Use async_on_unload Co-authored-by: Martin Hjelmare * Use async_on_unload Co-authored-by: Martin Hjelmare * Use async_setup_platforms Co-authored-by: Martin Hjelmare * Use async_unload_platforms Co-authored-by: Martin Hjelmare * Update homeassistant/components/xiaomi_miio/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/xiaomi_miio/const.py Co-authored-by: Martin Hjelmare * default use_cloud False * add options flow checks * fix styling * fix issort * add MiCloud check tests * fix indent * fix styling * fix tests * fix tests * fix black * re-write config flow * add explicit return type * update strings.json * black formatting * fix config flow Tested the config flow and it is now fully working * fix styling * Fix current tests * Add missing tests * fix styling * add re-auth flow * fix styling * fix reauth flow * Add reauth flow test * use ConfigEntryAuthFailed * also trigger reauth @ login error * fix styling * remove unused import * fix spelling Co-authored-by: Martin Hjelmare * Fix spelling Co-authored-by: Martin Hjelmare * fix spelling Co-authored-by: Martin Hjelmare * remove unessesary .keys() Co-authored-by: Martin Hjelmare * combine async_add_executor_job calls * remove async_step_model * fix wrong indent * fix gatway.py * fix tests Co-authored-by: Nathan Tilley Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/__init__.py | 75 ++- .../components/xiaomi_miio/config_flow.py | 353 ++++++++--- homeassistant/components/xiaomi_miio/const.py | 12 + .../components/xiaomi_miio/gateway.py | 84 ++- .../components/xiaomi_miio/manifest.json | 2 +- .../components/xiaomi_miio/strings.json | 55 +- .../xiaomi_miio/translations/en.json | 105 ++-- requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../xiaomi_miio/test_config_flow.py | 556 ++++++++++++++++-- 10 files changed, 1050 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index fa2dfcb9944..076aed4d30c 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -51,6 +51,30 @@ async def async_setup_entry( ) +def get_platforms(config_entry): + """Return the platforms belonging to a config_entry.""" + model = config_entry.data[CONF_MODEL] + flow_type = config_entry.data[CONF_FLOW_TYPE] + + if flow_type == CONF_GATEWAY: + return GATEWAY_PLATFORMS + if flow_type == CONF_DEVICE: + if model in MODELS_SWITCH: + return SWITCH_PLATFORMS + if model in MODELS_FAN: + return FAN_PLATFORMS + if model in MODELS_LIGHT: + return LIGHT_PLATFORMS + for vacuum_model in MODELS_VACUUM: + if model.startswith(vacuum_model): + return VACUUM_PLATFORMS + for air_monitor_model in MODELS_AIR_MONITOR: + if model.startswith(air_monitor_model): + return AIR_MONITOR_PLATFORMS + + return [] + + async def async_setup_gateway_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): @@ -64,8 +88,10 @@ async def async_setup_gateway_entry( if entry.unique_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) + entry.async_on_unload(entry.add_update_listener(update_listener)) + # Connect to gateway - gateway = ConnectXiaomiGateway(hass) + gateway = ConnectXiaomiGateway(hass, entry) if not await gateway.async_connect_gateway(host, token): return False gateway_info = gateway.gateway_info @@ -128,29 +154,36 @@ async def async_setup_device_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up the Xiaomi Miio device component from a config entry.""" - model = entry.data[CONF_MODEL] - - # Identify platforms to setup - platforms = [] - if model in MODELS_SWITCH: - platforms = SWITCH_PLATFORMS - elif model in MODELS_FAN: - platforms = FAN_PLATFORMS - elif model in MODELS_LIGHT: - platforms = LIGHT_PLATFORMS - for vacuum_model in MODELS_VACUUM: - if model.startswith(vacuum_model): - platforms = VACUUM_PLATFORMS - for air_monitor_model in MODELS_AIR_MONITOR: - if model.startswith(air_monitor_model): - platforms = AIR_MONITOR_PLATFORMS + platforms = get_platforms(entry) if not platforms: return False - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.config_entries.async_setup_platforms(entry, platforms) return True + + +async def async_unload_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Unload a config entry.""" + platforms = get_platforms(config_entry) + + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, platforms + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +async def update_listener( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 59eee0e6e04..790a82a0411 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -2,34 +2,98 @@ import logging from re import search +from micloud import MiCloud import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac from .const import ( + CONF_CLOUD_COUNTRY, + CONF_CLOUD_PASSWORD, + CONF_CLOUD_SUBDEVICES, + CONF_CLOUD_USERNAME, CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, CONF_MAC, + CONF_MANUAL, CONF_MODEL, + DEFAULT_CLOUD_COUNTRY, DOMAIN, MODELS_ALL, MODELS_ALL_DEVICES, MODELS_GATEWAY, + SERVER_COUNTRY_CODES, ) from .device import ConnectXiaomiDevice _LOGGER = logging.getLogger(__name__) -DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" - DEVICE_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), } DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) -DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)} +DEVICE_MODEL_CONFIG = vol.Schema({vol.Required(CONF_MODEL): vol.In(MODELS_ALL)}) +DEVICE_CLOUD_CONFIG = vol.Schema( + { + vol.Optional(CONF_CLOUD_USERNAME): str, + vol.Optional(CONF_CLOUD_PASSWORD): str, + vol.Optional(CONF_CLOUD_COUNTRY, default=DEFAULT_CLOUD_COUNTRY): vol.In( + SERVER_COUNTRY_CODES + ), + vol.Optional(CONF_MANUAL, default=False): bool, + } +) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Options for the component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Init object.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + errors = {} + if user_input is not None: + use_cloud = user_input.get(CONF_CLOUD_SUBDEVICES, False) + cloud_username = self.config_entry.data.get(CONF_CLOUD_USERNAME) + cloud_password = self.config_entry.data.get(CONF_CLOUD_PASSWORD) + cloud_country = self.config_entry.data.get(CONF_CLOUD_COUNTRY) + + if use_cloud and ( + not cloud_username or not cloud_password or not cloud_country + ): + errors["base"] = "cloud_credentials_incomplete" + # trigger re-auth flow + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=self.config_entry.data, + ) + ) + + if not errors: + return self.async_create_entry(title="", data=user_input) + + settings_schema = vol.Schema( + { + vol.Optional( + CONF_CLOUD_SUBDEVICES, + default=self.config_entry.options.get(CONF_CLOUD_SUBDEVICES, False), + ): bool + } + ) + + return self.async_show_form( + step_id="init", data_schema=settings_schema, errors=errors + ) class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -41,16 +105,51 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self.host = None self.mac = None + self.token = None + self.model = None + self.name = None + self.cloud_username = None + self.cloud_password = None + self.cloud_country = None + self.cloud_devices = {} + + @staticmethod + @callback + def async_get_options_flow(config_entry) -> OptionsFlowHandler: + """Get the options flow.""" + return OptionsFlowHandler(config_entry) + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an authentication error or missing cloud credentials.""" + self.host = user_input[CONF_HOST] + self.token = user_input[CONF_TOKEN] + self.mac = user_input[CONF_MAC] + self.model = user_input.get(CONF_MODEL) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is not None: + return await self.async_step_cloud() + return self.async_show_form( + step_id="reauth_confirm", data_schema=vol.Schema({}) + ) async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" - host = conf[CONF_HOST] - self.context.update({"title_placeholders": {"name": f"YAML import {host}"}}) - return await self.async_step_device(user_input=conf) + self.host = conf[CONF_HOST] + self.token = conf[CONF_TOKEN] + self.name = conf.get(CONF_NAME) + self.model = conf.get(CONF_MODEL) + + self.context.update( + {"title_placeholders": {"name": f"YAML import {self.host}"}} + ) + return await self.async_step_connect() async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - return await self.async_step_device() + return await self.async_step_cloud() async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" @@ -79,7 +178,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): {"title_placeholders": {"name": f"Gateway {self.host}"}} ) - return await self.async_step_device() + return await self.async_step_cloud() for device_model in MODELS_ALL_DEVICES: if name.startswith(device_model.replace(".", "-")): @@ -91,7 +190,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): {"title_placeholders": {"name": f"{device_model} {self.host}"}} ) - return await self.async_step_device() + return await self.async_step_cloud() # Discovered device is not yet supported _LOGGER.debug( @@ -101,76 +200,190 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="not_xiaomi_miio") - async def async_step_device(self, user_input=None): - """Handle a flow initialized by the user to configure a xiaomi miio device.""" + def extract_cloud_info(self, cloud_device_info): + """Extract the cloud info.""" + if self.host is None: + self.host = cloud_device_info["localip"] + if self.mac is None: + self.mac = format_mac(cloud_device_info["mac"]) + if self.model is None: + self.model = cloud_device_info["model"] + if self.name is None: + self.name = cloud_device_info["name"] + self.token = cloud_device_info["token"] + + async def async_step_cloud(self, user_input=None): + """Configure a xiaomi miio device through the Miio Cloud.""" errors = {} if user_input is not None: - token = user_input[CONF_TOKEN] - model = user_input.get(CONF_MODEL) + if user_input[CONF_MANUAL]: + return await self.async_step_manual() + + cloud_username = user_input.get(CONF_CLOUD_USERNAME) + cloud_password = user_input.get(CONF_CLOUD_PASSWORD) + cloud_country = user_input.get(CONF_CLOUD_COUNTRY) + + if not cloud_username or not cloud_password or not cloud_country: + errors["base"] = "cloud_credentials_incomplete" + return self.async_show_form( + step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors + ) + + miio_cloud = MiCloud(cloud_username, cloud_password) + if not await self.hass.async_add_executor_job(miio_cloud.login): + errors["base"] = "cloud_login_error" + return self.async_show_form( + step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors + ) + + devices_raw = await self.hass.async_add_executor_job( + miio_cloud.get_devices, cloud_country + ) + + if not devices_raw: + errors["base"] = "cloud_no_devices" + return self.async_show_form( + step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors + ) + + self.cloud_devices = {} + for device in devices_raw: + parent_id = device.get("parent_id") + if not parent_id: + name = device["name"] + model = device["model"] + list_name = f"{name} - {model}" + self.cloud_devices[list_name] = device + + self.cloud_username = cloud_username + self.cloud_password = cloud_password + self.cloud_country = cloud_country + + if self.host is not None: + for device in self.cloud_devices.values(): + cloud_host = device.get("localip") + if cloud_host == self.host: + self.extract_cloud_info(device) + return await self.async_step_connect() + + if len(self.cloud_devices) == 1: + self.extract_cloud_info(list(self.cloud_devices.values())[0]) + return await self.async_step_connect() + + return await self.async_step_select() + + return self.async_show_form( + step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors + ) + + async def async_step_select(self, user_input=None): + """Handle multiple cloud devices found.""" + errors = {} + if user_input is not None: + cloud_device = self.cloud_devices[user_input["select_device"]] + self.extract_cloud_info(cloud_device) + return await self.async_step_connect() + + select_schema = vol.Schema( + {vol.Required("select_device"): vol.In(list(self.cloud_devices))} + ) + + return self.async_show_form( + step_id="select", data_schema=select_schema, errors=errors + ) + + async def async_step_manual(self, user_input=None): + """Configure a xiaomi miio device Manually.""" + errors = {} + if user_input is not None: + self.token = user_input[CONF_TOKEN] if user_input.get(CONF_HOST): self.host = user_input[CONF_HOST] - # Try to connect to a Xiaomi Device. - connect_device_class = ConnectXiaomiDevice(self.hass) - await connect_device_class.async_connect_device(self.host, token) - device_info = connect_device_class.device_info - - if model is None and device_info is not None: - model = device_info.model - - if model is not None: - if self.mac is None and device_info is not None: - self.mac = format_mac(device_info.mac_address) - - # Setup Gateways - for gateway_model in MODELS_GATEWAY: - if model.startswith(gateway_model): - unique_id = self.mac - await self.async_set_unique_id( - unique_id, raise_on_progress=False - ) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=DEFAULT_GATEWAY_NAME, - data={ - CONF_FLOW_TYPE: CONF_GATEWAY, - CONF_HOST: self.host, - CONF_TOKEN: token, - CONF_MODEL: model, - CONF_MAC: self.mac, - }, - ) - - # Setup all other Miio Devices - name = user_input.get(CONF_NAME, model) - - for device_model in MODELS_ALL_DEVICES: - if model.startswith(device_model): - unique_id = self.mac - await self.async_set_unique_id( - unique_id, raise_on_progress=False - ) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=name, - data={ - CONF_FLOW_TYPE: CONF_DEVICE, - CONF_HOST: self.host, - CONF_TOKEN: token, - CONF_MODEL: model, - CONF_MAC: self.mac, - }, - ) - errors["base"] = "unknown_device" - else: - errors["base"] = "cannot_connect" + return await self.async_step_connect() if self.host: schema = vol.Schema(DEVICE_SETTINGS) else: schema = DEVICE_CONFIG - if errors: - schema = schema.extend(DEVICE_MODEL_CONFIG) + return self.async_show_form(step_id="manual", data_schema=schema, errors=errors) - return self.async_show_form(step_id="device", data_schema=schema, errors=errors) + async def async_step_connect(self, user_input=None): + """Connect to a xiaomi miio device.""" + errors = {} + if self.host is None or self.token is None: + return self.async_abort(reason="incomplete_info") + + if user_input is not None: + self.model = user_input[CONF_MODEL] + + # Try to connect to a Xiaomi Device. + connect_device_class = ConnectXiaomiDevice(self.hass) + await connect_device_class.async_connect_device(self.host, self.token) + device_info = connect_device_class.device_info + + if self.model is None and device_info is not None: + self.model = device_info.model + + if self.model is None: + errors["base"] = "cannot_connect" + return self.async_show_form( + step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors + ) + + if self.mac is None and device_info is not None: + self.mac = format_mac(device_info.mac_address) + + unique_id = self.mac + existing_entry = await self.async_set_unique_id( + unique_id, raise_on_progress=False + ) + if existing_entry: + data = existing_entry.data.copy() + data[CONF_HOST] = self.host + data[CONF_TOKEN] = self.token + if ( + self.cloud_username is not None + and self.cloud_password is not None + and self.cloud_country is not None + ): + data[CONF_CLOUD_USERNAME] = self.cloud_username + data[CONF_CLOUD_PASSWORD] = self.cloud_password + data[CONF_CLOUD_COUNTRY] = self.cloud_country + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + if self.name is None: + self.name = self.model + + flow_type = None + for gateway_model in MODELS_GATEWAY: + if self.model.startswith(gateway_model): + flow_type = CONF_GATEWAY + + if flow_type is None: + for device_model in MODELS_ALL_DEVICES: + if self.model.startswith(device_model): + flow_type = CONF_DEVICE + + if flow_type is not None: + return self.async_create_entry( + title=self.name, + data={ + CONF_FLOW_TYPE: flow_type, + CONF_HOST: self.host, + CONF_TOKEN: self.token, + CONF_MODEL: self.model, + CONF_MAC: self.mac, + CONF_CLOUD_USERNAME: self.cloud_username, + CONF_CLOUD_PASSWORD: self.cloud_password, + CONF_CLOUD_COUNTRY: self.cloud_country, + }, + ) + + errors["base"] = "unknown_device" + return self.async_show_form( + step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors + ) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 35c4d4a1662..27d0a34bf39 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -1,16 +1,28 @@ """Constants for the Xiaomi Miio component.""" DOMAIN = "xiaomi_miio" +# Config flow CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" CONF_DEVICE = "device" CONF_MODEL = "model" CONF_MAC = "mac" +CONF_CLOUD_USERNAME = "cloud_username" +CONF_CLOUD_PASSWORD = "cloud_password" +CONF_CLOUD_COUNTRY = "cloud_country" +CONF_MANUAL = "manual" + +# Options flow +CONF_CLOUD_SUBDEVICES = "cloud_subdevices" KEY_COORDINATOR = "coordinator" ATTR_AVAILABLE = "available" +# Cloud +SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"] +DEFAULT_CLOUD_COUNTRY = "cn" + # Fan Models MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index be96f77240a..7482dae5d77 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -1,12 +1,22 @@ """Code to handle a Xiaomi Gateway.""" import logging +from micloud import MiCloud from miio import DeviceException, gateway +from miio.gateway.gateway import GATEWAY_MODEL_EU +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_AVAILABLE, DOMAIN +from .const import ( + ATTR_AVAILABLE, + CONF_CLOUD_COUNTRY, + CONF_CLOUD_PASSWORD, + CONF_CLOUD_SUBDEVICES, + CONF_CLOUD_USERNAME, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -14,11 +24,18 @@ _LOGGER = logging.getLogger(__name__) class ConnectXiaomiGateway: """Class to async connect to a Xiaomi Gateway.""" - def __init__(self, hass): + def __init__(self, hass, config_entry): """Initialize the entity.""" self._hass = hass + self._config_entry = config_entry self._gateway_device = None self._gateway_info = None + self._use_cloud = None + self._cloud_username = None + self._cloud_password = None + self._cloud_country = None + self._host = None + self._token = None @property def gateway_device(self): @@ -33,21 +50,17 @@ class ConnectXiaomiGateway: async def async_connect_gateway(self, host, token): """Connect to the Xiaomi Gateway.""" _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - try: - self._gateway_device = gateway.Gateway(host, token) - # get the gateway info - self._gateway_info = await self._hass.async_add_executor_job( - self._gateway_device.info - ) - # get the connected sub devices - await self._hass.async_add_executor_job( - self._gateway_device.discover_devices - ) - except DeviceException: - _LOGGER.error( - "DeviceException during setup of xiaomi gateway with host %s", host - ) + + self._host = host + self._token = token + self._use_cloud = self._config_entry.options.get(CONF_CLOUD_SUBDEVICES, False) + self._cloud_username = self._config_entry.data.get(CONF_CLOUD_USERNAME) + self._cloud_password = self._config_entry.data.get(CONF_CLOUD_PASSWORD) + self._cloud_country = self._config_entry.data.get(CONF_CLOUD_COUNTRY) + + if not await self._hass.async_add_executor_job(self.connect_gateway): return False + _LOGGER.debug( "%s %s %s detected", self._gateway_info.model, @@ -56,6 +69,45 @@ class ConnectXiaomiGateway: ) return True + def connect_gateway(self): + """Connect the gateway in a way that can called by async_add_executor_job.""" + try: + self._gateway_device = gateway.Gateway(self._host, self._token) + # get the gateway info + self._gateway_device.info() + + # get the connected sub devices + if self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU: + if ( + self._cloud_username is None + or self._cloud_password is None + or self._cloud_country is None + ): + raise ConfigEntryAuthFailed( + "Missing cloud credentials in Xiaomi Miio configuration" + ) + + # use miio-cloud + miio_cloud = MiCloud(self._cloud_username, self._cloud_password) + if not miio_cloud.login(): + raise ConfigEntryAuthFailed( + "Could not login to Xioami Miio Cloud, check the credentials" + ) + devices_raw = miio_cloud.get_devices(self._cloud_country) + self._gateway_device.get_devices_from_dict(devices_raw) + else: + # use local query (not supported by all gateway types) + self._gateway_device.discover_devices() + + except DeviceException: + _LOGGER.error( + "DeviceException during setup of xiaomi gateway with host %s", + self._host, + ) + return False + + return True + class XiaomiGatewayDevice(CoordinatorEntity, Entity): """Representation of a base Xiaomi Gateway Device.""" diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 939e30edda8..1f37d624b95 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "python-miio==0.5.6"], + "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.6"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 571df98eef1..69a1621c973 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -1,23 +1,70 @@ { "config": { "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "incomplete_info": "Incomplete information to setup device, no host or token supplied.", + "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown_device": "The device model is not known, not able to setup the device using config flow." + "unknown_device": "The device model is not known, not able to setup the device using config flow.", + "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", + "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials." }, "flow_title": "{name}", "step": { - "device": { + "reauth_confirm": { + "description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.", + "title": "[%key:common::config_flow::title::reauth%]" + }, + "cloud": { + "data": { + "cloud_username": "Cloud username", + "cloud_password": "Cloud password", + "cloud_country": "Cloud server country", + "manual": "Configure manually (not recommended)" + }, + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "select": { + "data": { + "select_device": "Miio device" + }, + "description": "Select the Xiaomi Miio device to setup.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "manual": { "data": { "host": "[%key:common::config_flow::data::ip%]", - "model": "Device model (Optional)", "token": "[%key:common::config_flow::data::api_token%]" }, "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Device model" + }, + "description": "Manually select the device model from the supported models.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + } + } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country" + }, + "step": { + "init": { + "title": "Xiaomi Miio", + "description": "Specify optional settings", + "data": { + "cloud_subdevices": "Use cloud to get connected subdevices" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index f5629a86eca..9d6a3afbb60 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -1,42 +1,71 @@ { - "config": { - "abort": { - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress" + "config": { + "abort": { + "reauth_successful": "Re-authentication was successful", + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "incomplete_info": "Incomplete information to setup device, no host or token supplied.", + "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio." + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown_device": "The device model is not known, not able to setup the device using config flow.", + "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", + "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials." + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "description": "The Xiaomi Miio integration needs to re-authenticate your acount in order to update the tokens or add missing cloud credentials.", + "title": "Reauthenticate Integration" + }, + "cloud": { + "data": { + "cloud_username": "Cloud username", + "cloud_password": "Cloud password", + "cloud_country": "Cloud server country", + "manual": "Configure manually (not recommended)" }, - "error": { - "cannot_connect": "Failed to connect", - "no_device_selected": "No device selected, please select one device.", - "unknown_device": "The device model is not known, not able to setup the device using config flow." + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "select": { + "data": { + "select_device": "Miio device" }, - "flow_title": "{name}", - "step": { - "device": { - "data": { - "host": "IP Address", - "model": "Device model (Optional)", - "name": "Name of the device", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP Address", - "name": "Name of the Gateway", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connect to a Xiaomi Gateway" - }, - "description": "Select to which device you want to connect.", - "title": "Xiaomi Miio" - } - } + "description": "Select the Xiaomi Miio device to setup.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "manual": { + "data": { + "host": "IP Address", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Device model" + }, + "description": "Manually select the device model from the supported models.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + } } -} \ No newline at end of file + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country" + }, + "step": { + "init": { + "title": "Xiaomi Miio", + "description": "Specify optional settings", + "data": { + "cloud_subdevices": "Use cloud to get connected subdevices" + } + } + } + } +} diff --git a/requirements_all.txt b/requirements_all.txt index b735fd3e431..78b6eb8fe81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -956,6 +956,9 @@ meteofrance-api==1.0.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.xiaomi_miio +micloud==0.3 + # homeassistant.components.miflora miflora==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 727b3cf9b46..f0ba017a98f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -528,6 +528,9 @@ meteofrance-api==1.0.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.xiaomi_miio +micloud==0.3 + # homeassistant.components.mill millheater==0.4.1 diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index de1ccbf1a8b..68091efffa1 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -2,27 +2,86 @@ from unittest.mock import Mock, patch from miio import DeviceException +import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.xiaomi_miio import const -from homeassistant.components.xiaomi_miio.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from tests.common import MockConfigEntry + ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" ZEROCONF_MAC = "mac" TEST_HOST = "1.2.3.4" +TEST_HOST2 = "5.6.7.8" +TEST_CLOUD_USER = "username" +TEST_CLOUD_PASS = "password" +TEST_CLOUD_COUNTRY = "cn" TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" +TEST_NAME2 = "Test_Gateway_2" TEST_MODEL = const.MODELS_GATEWAY[0] TEST_MAC = "ab:cd:ef:gh:ij:kl" +TEST_MAC2 = "mn:op:qr:st:uv:wx" TEST_MAC_DEVICE = "abcdefghijkl" +TEST_MAC_DEVICE2 = "mnopqrstuvwx" TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" TEST_FIRMWARE_VERSION = "1.2.3_456" TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local." TEST_SUB_DEVICE_LIST = [] +TEST_CLOUD_DEVICES_1 = [ + { + "parent_id": None, + "name": TEST_NAME, + "model": TEST_MODEL, + "localip": TEST_HOST, + "mac": TEST_MAC_DEVICE, + "token": TEST_TOKEN, + } +] +TEST_CLOUD_DEVICES_2 = [ + { + "parent_id": None, + "name": TEST_NAME, + "model": TEST_MODEL, + "localip": TEST_HOST, + "mac": TEST_MAC_DEVICE, + "token": TEST_TOKEN, + }, + { + "parent_id": None, + "name": TEST_NAME2, + "model": TEST_MODEL, + "localip": TEST_HOST2, + "mac": TEST_MAC_DEVICE2, + "token": TEST_TOKEN, + }, +] + + +@pytest.fixture(name="xiaomi_miio_connect", autouse=True) +def xiaomi_miio_connect_fixture(): + """Mock denonavr connection and entry setup.""" + mock_info = get_mock_info() + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login", + return_value=True, + ), patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", + return_value=TEST_CLOUD_DEVICES_1, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.xiaomi_miio.async_unload_entry", return_value=True + ): + yield def get_mock_info( @@ -48,7 +107,16 @@ async def test_config_flow_step_gateway_connect_error(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} with patch( @@ -61,7 +129,7 @@ async def test_config_flow_step_gateway_connect_error(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -72,26 +140,30 @@ async def test_config_flow_gateway_success(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" assert result["errors"] == {} - mock_info = get_mock_info() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) - with patch( - "homeassistant.components.xiaomi_miio.device.Device.info", - return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, - ) + assert result["type"] == "form" + assert result["step_id"] == "manual" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["title"] == TEST_MODEL assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: TEST_MODEL, @@ -99,6 +171,202 @@ async def test_config_flow_gateway_success(hass): } +async def test_config_flow_gateway_cloud_success(hass): + """Test a successful config flow using cloud.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, + } + + +async def test_config_flow_gateway_cloud_multiple_success(hass): + """Test a successful config flow using cloud with multiple devices.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", + return_value=TEST_CLOUD_DEVICES_2, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "select" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"select_device": f"{TEST_NAME2} - {TEST_MODEL}"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME2 + assert result["data"] == { + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + CONF_HOST: TEST_HOST2, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC2, + } + + +async def test_config_flow_gateway_cloud_incomplete(hass): + """Test a failed config flow using incomplete cloud credentials.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {"base": "cloud_credentials_incomplete"} + + +async def test_config_flow_gateway_cloud_login_error(hass): + """Test a failed config flow using cloud login error.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {"base": "cloud_login_error"} + + +async def test_config_flow_gateway_cloud_no_devices(hass): + """Test a failed config flow using cloud with no devices.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + with patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", + return_value=[], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {"base": "cloud_no_devices"} + + +async def test_config_flow_gateway_cloud_missing_token(hass): + """Test a failed config flow using cloud with a missing token.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + cloud_device = [ + { + "parent_id": None, + "name": TEST_NAME, + "model": TEST_MODEL, + "localip": TEST_HOST, + "mac": TEST_MAC_DEVICE, + "token": None, + } + ] + + with patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", + return_value=cloud_device, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "incomplete_info" + + async def test_zeroconf_gateway_success(hass): """Test a successful zeroconf discovery of a gateway.""" result = await hass.config_entries.flow.async_init( @@ -112,26 +380,25 @@ async def test_zeroconf_gateway_success(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" assert result["errors"] == {} - mock_info = get_mock_info() - - with patch( - "homeassistant.components.xiaomi_miio.device.Device.info", - return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: TEST_TOKEN}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: TEST_MODEL, @@ -184,7 +451,16 @@ async def test_config_flow_step_device_connect_error(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} with patch( @@ -197,7 +473,7 @@ async def test_config_flow_step_device_connect_error(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} @@ -208,7 +484,16 @@ async def test_config_flow_step_unknown_device(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} mock_info = get_mock_info(model="UNKNOWN") @@ -223,7 +508,7 @@ async def test_config_flow_step_unknown_device(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "connect" assert result["errors"] == {"base": "unknown_device"} @@ -234,8 +519,6 @@ async def test_import_flow_success(hass): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( const.DOMAIN, @@ -247,6 +530,9 @@ async def test_import_flow_success(hass): assert result["title"] == TEST_NAME assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: const.MODELS_SWITCH[0], @@ -261,7 +547,16 @@ async def test_config_flow_step_device_manual_model_succes(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} with patch( @@ -274,7 +569,7 @@ async def test_config_flow_step_device_manual_model_succes(hass): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} overwrite_model = const.MODELS_VACUUM[0] @@ -282,18 +577,19 @@ async def test_config_flow_step_device_manual_model_succes(hass): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", side_effect=DeviceException({}), - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model}, + {const.CONF_MODEL: overwrite_model}, ) assert result["type"] == "create_entry" assert result["title"] == overwrite_model assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model, @@ -308,7 +604,16 @@ async def config_flow_device_success(hass, model_to_test): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} mock_info = get_mock_info(model=model_to_test) @@ -316,8 +621,6 @@ async def config_flow_device_success(hass, model_to_test): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -328,6 +631,9 @@ async def config_flow_device_success(hass, model_to_test): assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: model_to_test, @@ -348,7 +654,16 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): ) assert result["type"] == "form" - assert result["step_id"] == "device" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" assert result["errors"] == {} mock_info = get_mock_info(model=model_to_test) @@ -356,8 +671,6 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -368,6 +681,9 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): assert result["title"] == model_to_test assert result["data"] == { const.CONF_FLOW_TYPE: const.CONF_DEVICE, + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: model_to_test, @@ -399,3 +715,147 @@ async def test_zeroconf_vacuum_success(hass): test_vacuum_model = const.MODELS_VACUUM[0] test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-") await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model) + + +async def test_options_flow(hass): + """Test specifying non default settings using options flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={ + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, + }, + title=TEST_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + 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"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + const.CONF_CLOUD_SUBDEVICES: True, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + const.CONF_CLOUD_SUBDEVICES: True, + } + + +async def test_options_flow_incomplete(hass): + """Test specifying incomplete settings using options flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={ + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, + }, + title=TEST_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + 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"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + const.CONF_CLOUD_SUBDEVICES: True, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] == {"base": "cloud_credentials_incomplete"} + + +async def test_reauth(hass): + """Test a reauth flow.""" + # await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={ + const.CONF_CLOUD_USERNAME: None, + const.CONF_CLOUD_PASSWORD: None, + const.CONF_CLOUD_COUNTRY: None, + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, + }, + title=TEST_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=config_entry.data, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + + config_data = config_entry.data.copy() + assert config_data == { + const.CONF_FLOW_TYPE: const.CONF_GATEWAY, + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + const.CONF_MODEL: TEST_MODEL, + const.CONF_MAC: TEST_MAC, + } From e1538594cdc03e876c344bd59cb63398e11a4f0d Mon Sep 17 00:00:00 2001 From: Kim Frellsen Date: Tue, 15 Jun 2021 00:29:37 +0200 Subject: [PATCH 365/750] Update fortios device tracker to support FortiOS 7.0 (#51640) Co-authored-by: Martin Hjelmare --- .../components/fortios/device_tracker.py | 39 +++++++++++++++---- .../components/fortios/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 2b2d14f60e0..1b9134bee44 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -46,29 +46,50 @@ def get_scanner(hass, config): _LOGGER.error("Failed to login to FortiOS API: %s", ex) return None - return FortiOSDeviceScanner(fgt) + status_json = fgt.monitor("system/status", "") + fos_major_version = int(status_json["version"][1]) + + if fos_major_version < 6 or fos_major_version > 7: + _LOGGER.error( + "Unsupported FortiOS version, fos_major_version = %s", + fos_major_version, + ) + return None + + api_url = "user/device/query" + if fos_major_version == 6: + api_url = "user/device/select" + + return FortiOSDeviceScanner(fgt, fos_major_version, api_url) class FortiOSDeviceScanner(DeviceScanner): """This class queries a FortiOS unit for connected devices.""" - def __init__(self, fgt) -> None: + def __init__(self, fgt, fos_major_version, api_url) -> None: """Initialize the scanner.""" self._clients = {} self._clients_json = {} self._fgt = fgt + self._fos_major_version = fos_major_version + self._api_url = api_url def update(self): """Update clients from the device.""" - clients_json = self._fgt.monitor("user/device/select", "") + clients_json = self._fgt.monitor(self._api_url, "") self._clients_json = clients_json self._clients = [] if clients_json: - for client in clients_json["results"]: - if client["last_seen"] < 180: - self._clients.append(client["mac"].upper()) + if self._fos_major_version == 6: + for client in clients_json["results"]: + if client["last_seen"] < 180: + self._clients.append(client["mac"].upper()) + elif self._fos_major_version == 7: + for client in clients_json["results"]: + if client["is_online"]: + self._clients.append(client["mac"].upper()) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -90,7 +111,11 @@ class FortiOSDeviceScanner(DeviceScanner): for client in data["results"]: if client["mac"] == device: try: - name = client["host"]["name"] + name = "" + if self._fos_major_version == 6: + name = client["host"]["name"] + elif self._fos_major_version == 7: + name = client["hostname"] _LOGGER.debug("Getting device name=%s", name) return name except KeyError as kex: diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index 251cb900adc..cc351441cdd 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -2,7 +2,7 @@ "domain": "fortios", "name": "FortiOS", "documentation": "https://www.home-assistant.io/integrations/fortios/", - "requirements": ["fortiosapi==0.10.8"], + "requirements": ["fortiosapi==1.0.5"], "codeowners": ["@kimfrellsen"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 78b6eb8fe81..03a5fff0f60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -624,7 +624,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.fortios -fortiosapi==0.10.8 +fortiosapi==1.0.5 # homeassistant.components.freebox freebox-api==0.0.10 From dfe21eb78ff90aafcb1d36a9c2a5d048d0a0cf73 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 15 Jun 2021 00:56:14 +0200 Subject: [PATCH 366/750] Add selectors to BMW Connected Drive service definitions (#47065) Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 19 +++++- .../bmw_connected_drive/services.yaml | 64 +++++++++++++------ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 038a8696818..599892d6a03 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + CONF_DEVICE_ID, CONF_NAME, CONF_PASSWORD, CONF_REGION, @@ -18,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import discovery +from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import track_utc_time_change @@ -51,7 +52,12 @@ ACCOUNT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLOW_EXTRA) -SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) +SERVICE_SCHEMA = vol.Schema( + vol.Any( + {vol.Required(ATTR_VIN): cv.string}, + {vol.Required(CONF_DEVICE_ID): cv.string}, + ) +) DEFAULT_OPTIONS = { CONF_READ_ONLY: False, @@ -207,8 +213,15 @@ def setup_account(entry: ConfigEntry, hass, name: str) -> BMWConnectedDriveAccou def execute_service(call): """Execute a service for a vehicle.""" - vin = call.data[ATTR_VIN] + vin = call.data.get(ATTR_VIN) + device_id = call.data.get(CONF_DEVICE_ID) + vehicle = None + + if not vin and device_id: + device = device_registry.async_get(hass).async_get(device_id) + vin = next(iter(device.identifiers))[1] + # Double check for read_only accounts as another account could create the services for entry_data in [ e diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 563e14e5577..964fb8ab39b 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -6,14 +6,20 @@ light_flash: name: Flash lights description: > - Flash the lights of the vehicle. The vehicle is identified via the vin - (see below). + Flash the lights of the vehicle. The vehicle is identified either via its + device entry or the VIN. If a VIN is specified, the device entry will be ignored. fields: + device_id: + name: Car + description: The BMW Connected Drive device + selector: + device: + integration: bmw_connected_drive vin: name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters - required: true + description: The vehicle identification number (VIN) of the vehicle, 17 characters + advanced: true + required: false example: WBANXXXXXX1234567 selector: text: @@ -21,14 +27,20 @@ light_flash: sound_horn: name: Sound horn description: > - Sound the horn of the vehicle. The vehicle is identified via the vin - (see below). + Sound the horn of the vehicle. The vehicle is identified either via its + device entry or the VIN. If a VIN is specified, the device entry will be ignored. fields: + device_id: + name: Car + description: The BMW Connected Drive device + selector: + device: + integration: bmw_connected_drive vin: name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters - required: true + description: The vehicle identification number (VIN) of the vehicle, 17 characters + advanced: true + required: false example: WBANXXXXXX1234567 selector: text: @@ -38,14 +50,20 @@ activate_air_conditioning: description: > Start the air conditioning of the vehicle. What exactly is started here depends on the type of vehicle. It might range from just ventilation over - auxiliary heating to real air conditioning. The vehicle is identified via - the vin (see below). + auxiliary heating to real air conditioning. The vehicle is identified either via its + device entry or the VIN. If a VIN is specified, the device entry will be ignored. fields: + device_id: + name: Car + description: The BMW Connected Drive device + selector: + device: + integration: bmw_connected_drive vin: name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters - required: true + description: The vehicle identification number (VIN) of the vehicle, 17 characters + advanced: true + required: false example: WBANXXXXXX1234567 selector: text: @@ -53,14 +71,20 @@ activate_air_conditioning: find_vehicle: name: Find vehicle description: > - Request vehicle to update the gps location. The vehicle is identified via the vin - (see below). + Request vehicle to update the GPS location. The vehicle is identified either via its + device entry or the VIN. If a VIN is specified, the device entry will be ignored. fields: + device_id: + name: Car + description: The BMW Connected Drive device + selector: + device: + integration: bmw_connected_drive vin: name: VIN - description: > - The vehicle identification number (VIN) of the vehicle, 17 characters - required: true + description: The vehicle identification number (VIN) of the vehicle, 17 characters + advanced: true + required: false example: WBANXXXXXX1234567 selector: text: From 5469cc8fb27e6fe1b82e95c48b4a8bbc35f162e1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 15 Jun 2021 00:11:31 +0000 Subject: [PATCH 367/750] [ci skip] Translation update --- .../components/ambee/translations/no.json | 9 + .../components/kraken/translations/et.json | 2 +- .../components/roomba/translations/no.json | 4 +- .../components/roomba/translations/ru.json | 4 +- .../components/wled/translations/no.json | 9 + .../xiaomi_miio/translations/en.json | 159 ++++++++++-------- .../yamaha_musiccast/translations/ca.json | 23 +++ .../yamaha_musiccast/translations/et.json | 23 +++ .../yamaha_musiccast/translations/nl.json | 23 +++ .../yamaha_musiccast/translations/no.json | 23 +++ .../yamaha_musiccast/translations/ru.json | 23 +++ .../translations/zh-Hant.json | 23 +++ 12 files changed, 254 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/yamaha_musiccast/translations/ca.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/et.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/nl.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/no.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/ru.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json index bdfd629ae10..b735ee91509 100644 --- a/homeassistant/components/ambee/translations/no.json +++ b/homeassistant/components/ambee/translations/no.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel", + "description": "Autentiser p\u00e5 nytt med Ambee-kontoen din." + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/kraken/translations/et.json b/homeassistant/components/kraken/translations/et.json index 74693aa9525..d0603f324bf 100644 --- a/homeassistant/components/kraken/translations/et.json +++ b/homeassistant/components/kraken/translations/et.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kinnita s\u00e4tted" + "description": "Kas soovid alustada seadistamist?" } } }, diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 2caba79f50c..1cacffdf425 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -45,8 +45,8 @@ "host": "Vert", "password": "Passord" }, - "description": "Henting av BLID og passord er en manuell prosess. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Koble til enheten" + "description": "Velg en Roomba eller Braava.", + "title": "Koble automatisk til enheten" } } }, diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 6ff4feb4e9c..f61ecde08ec 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -45,8 +45,8 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } }, diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index 0e5df905e29..a63871613cc 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -20,5 +20,14 @@ "title": "Oppdaget WLED-enhet" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Behold hovedlys, selv med 1 LED-segment." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 9d6a3afbb60..cbe10230093 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -1,71 +1,98 @@ { - "config": { - "abort": { - "reauth_successful": "Re-authentication was successful", - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", - "incomplete_info": "Incomplete information to setup device, no host or token supplied.", - "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio." - }, - "error": { - "cannot_connect": "Failed to connect", - "unknown_device": "The device model is not known, not able to setup the device using config flow.", - "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", - "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", - "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials." - }, - "flow_title": "{name}", - "step": { - "reauth_confirm": { - "description": "The Xiaomi Miio integration needs to re-authenticate your acount in order to update the tokens or add missing cloud credentials.", - "title": "Reauthenticate Integration" - }, - "cloud": { - "data": { - "cloud_username": "Cloud username", - "cloud_password": "Cloud password", - "cloud_country": "Cloud server country", - "manual": "Configure manually (not recommended)" + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "incomplete_info": "Incomplete information to setup device, no host or token supplied.", + "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.", + "reauth_successful": "Re-authentication was successful" }, - "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "select": { - "data": { - "select_device": "Miio device" + "error": { + "cannot_connect": "Failed to connect", + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", + "cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials.", + "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", + "no_device_selected": "No device selected, please select one device.", + "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "description": "Select the Xiaomi Miio device to setup.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "manual": { - "data": { - "host": "IP Address", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "connect": { - "data": { - "model": "Device model" - }, - "description": "Manually select the device model from the supported models.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - } - } - }, - "options": { - "error": { - "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country" - }, - "step": { - "init": { - "title": "Xiaomi Miio", - "description": "Specify optional settings", - "data": { - "cloud_subdevices": "Use cloud to get connected subdevices" + "flow_title": "{name}", + "step": { + "cloud": { + "data": { + "cloud_country": "Cloud server country", + "cloud_password": "Cloud password", + "cloud_username": "Cloud username", + "manual": "Configure manually (not recommended)" + }, + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Device model" + }, + "description": "Manually select the device model from the supported models.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "device": { + "data": { + "host": "IP Address", + "model": "Device model (Optional)", + "name": "Name of the device", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "gateway": { + "data": { + "host": "IP Address", + "name": "Name of the Gateway", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Gateway" + }, + "manual": { + "data": { + "host": "IP Address", + "token": "API Token" + }, + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.", + "title": "Reauthenticate Integration" + }, + "select": { + "data": { + "select_device": "Miio device" + }, + "description": "Select the Xiaomi Miio device to setup.", + "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Connect to a Xiaomi Gateway" + }, + "description": "Select to which device you want to connect.", + "title": "Xiaomi Miio" + } + } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Use cloud to get connected subdevices" + }, + "description": "Specify optional settings", + "title": "Xiaomi Miio" + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json new file mode 100644 index 00000000000..32cd231c963 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "yxc_control_url_missing": "No s'ha proporcionat l'URL de control en la descripci\u00f3 ssdp." + }, + "error": { + "no_musiccast_device": "Aquest dispositiu sembla no ser un dispositiu MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura MusicCast per a integrar-lo amb Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/et.json b/homeassistant/components/yamaha_musiccast/translations/et.json new file mode 100644 index 00000000000..8283298f11b --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "yxc_control_url_missing": "Juhtelemendi URL-i pole ssdp kirjelduses esitatud." + }, + "error": { + "no_musiccast_device": "Tundub, et leitud seade pole MusicCasti seade." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Seadista MusicCast'i sidumine Home Assistantiga." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json new file mode 100644 index 00000000000..e1e31149c06 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "yxc_control_url_missing": "De controle-URL wordt niet gegeven in de ssdp-beschrijving." + }, + "error": { + "no_musiccast_device": "Dit apparaat lijkt geen MusicCast-apparaat te zijn." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Wil je beginnen met instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Stel MusicCast in om te integreren met Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/no.json b/homeassistant/components/yamaha_musiccast/translations/no.json new file mode 100644 index 00000000000..5381cdc5d67 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "yxc_control_url_missing": "Kontroll-URL-en er ikke gitt i ssdp-beskrivelsen." + }, + "error": { + "no_musiccast_device": "Denne enheten ser ut til \u00e5 v\u00e6re ingen MusicCast-enhet." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Sett opp MusicCast for \u00e5 integrere med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json new file mode 100644 index 00000000000..161a97ad15d --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "yxc_control_url_missing": "URL-\u0430\u0434\u0440\u0435\u0441 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 ssdp." + }, + "error": { + "no_musiccast_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "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 MusicCast." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json new file mode 100644 index 00000000000..45664e0b815 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "yxc_control_url_missing": "SSDP \u63cf\u8ff0\u4e2d\u672a\u5305\u542b\u63a7\u5236 URL\u3002" + }, + "error": { + "no_musiccast_device": "\u6b64\u88dd\u7f6e\u4f3c\u4e4e\u4e0d\u662f MusicCast \u88dd\u7f6e\u3002" + }, + "flow_title": "MusicCast\uff1a{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a MusicCast \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + } + } + } +} \ No newline at end of file From 59a3e0f4dc93b8c25d727f1371a9fb80f16bf5b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Jun 2021 02:23:13 +0200 Subject: [PATCH 368/750] Improve editing of device conditions referencing non-added HVAC (#51832) --- .../components/climate/device_condition.py | 30 ++- .../climate/test_device_condition.py | 246 +++++++++++------- 2 files changed, 175 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 3792f1219a4..3fc6d94ba4f 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -5,16 +5,16 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.entity import get_capability, get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, const @@ -52,7 +52,7 @@ async def async_get_conditions( if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) + supported_features = get_supported_features(hass, entry.entity_id) base_condition = { CONF_CONDITION: "device", @@ -63,10 +63,7 @@ async def async_get_conditions( conditions.append({**base_condition, CONF_TYPE: "is_hvac_mode"}) - if ( - state - and state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_PRESET_MODE - ): + if supported_features & const.SUPPORT_PRESET_MODE: conditions.append({**base_condition, CONF_TYPE: "is_preset_mode"}) return conditions @@ -95,21 +92,28 @@ def async_condition_from_config( async def async_get_condition_capabilities(hass, config): """List condition capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) condition_type = config[CONF_TYPE] fields = {} if condition_type == "is_hvac_mode": - hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + try: + hvac_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_HVAC_MODES) + or [] + ) + except HomeAssistantError: + hvac_modes = [] fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) elif condition_type == "is_preset_mode": - if state: - preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) - else: + try: + preset_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_PRESET_MODES) + or [] + ) + except HomeAssistantError: preset_modes = [] - fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes) return {"extra_fields": vol.Schema(fields)} diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 27341b6c2c9..2d6aa82fdf1 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -36,7 +36,24 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_condition_types", + [ + (False, 0, 0, ["is_hvac_mode"]), + (False, const.SUPPORT_PRESET_MODE, 0, ["is_hvac_mode", "is_preset_mode"]), + (True, 0, 0, ["is_hvac_mode"]), + (True, 0, const.SUPPORT_PRESET_MODE, ["is_hvac_mode", "is_preset_mode"]), + ], +) +async def test_get_conditions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_condition_types, +): """Test we get the expected conditions from a climate.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -44,64 +61,27 @@ async def test_get_conditions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - f"{DOMAIN}.test_5678", - const.HVAC_MODE_COOL, - { - const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, - const.ATTR_PRESET_MODE: const.PRESET_AWAY, - const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], - }, + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) - expected_conditions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + expected_conditions = [] + expected_conditions += [ { "condition": "device", "domain": DOMAIN, - "type": "is_hvac_mode", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_preset_mode", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert_lists_same(conditions, expected_conditions) - - -async def test_get_conditions_hvac_only(hass, device_reg, entity_reg): - """Test we get the expected conditions from a climate.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set( - f"{DOMAIN}.test_5678", - const.HVAC_MODE_COOL, - { - const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, - const.ATTR_PRESET_MODE: const.PRESET_AWAY, - const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], - }, - ) - hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_hvac_mode", + "type": condition, "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", } + for condition in expected_condition_types ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) @@ -204,65 +184,155 @@ async def test_if_state(hass, calls): assert len(calls) == 2 -async def test_capabilities(hass): - """Bla.""" - hass.states.async_set( - "climate.entity", - const.HVAC_MODE_COOL, - { - const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, - const.ATTR_PRESET_MODE: const.PRESET_AWAY, - const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], - const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], - }, +@pytest.mark.parametrize( + "set_state,capabilities_reg,capabilities_state,condition,expected_capabilities", + [ + ( + False, + {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + {}, + "is_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + {}, + "is_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF]}, + "is_hvac_mode", + [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY]}, + "is_preset_mode", + [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ], +) +async def test_capabilities( + hass, + device_reg, + entity_reg, + set_state, + capabilities_reg, + capabilities_state, + condition, + expected_capabilities, +): + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + capabilities=capabilities_reg, + ) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + capabilities_state, + ) - # Test hvac mode capabilities = await device_condition.async_get_condition_capabilities( hass, { "condition": "device", "domain": DOMAIN, - "device_id": "", - "entity_id": "climate.entity", - "type": "is_hvac_mode", + "device_id": "abcdefgh", + "entity_id": f"{DOMAIN}.test_5678", + "type": condition, }, ) assert capabilities and "extra_fields" in capabilities - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ - { - "name": "hvac_mode", - "options": [("cool", "cool"), ("off", "off")], - "required": True, - "type": "select", - } - ] + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) + + +@pytest.mark.parametrize( + "condition,capability_name", + [("is_hvac_mode", "hvac_mode"), ("is_preset_mode", "preset_mode")], +) +async def test_capabilities_missing_entity( + hass, device_reg, entity_reg, condition, capability_name +): + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) - # Test preset mode capabilities = await device_condition.async_get_condition_capabilities( hass, { "condition": "device", "domain": DOMAIN, - "device_id": "", - "entity_id": "climate.entity", - "type": "is_preset_mode", + "device_id": "abcdefgh", + "entity_id": f"{DOMAIN}.test_5678", + "type": condition, }, ) - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ + expected_capabilities = [ { - "name": "preset_mode", - "options": [("home", "home"), ("away", "away")], + "name": capability_name, + "options": [], "required": True, "type": "select", } ] + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) From 3db8d9ede5bea8da9daa55e52032813b49d89e95 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 15 Jun 2021 03:34:04 -0400 Subject: [PATCH 369/750] Require admin for new node status WS API command (#51863) --- homeassistant/components/zwave_js/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1b4a44d8436..5cf3ba32411 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -255,6 +255,7 @@ async def websocket_node_status( ) +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_state", From f3c6e846fab8007f2ba3129d7faa2bc13dcf0882 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Jun 2021 12:10:47 +0200 Subject: [PATCH 370/750] Enable asyncio debugging from debugpy integration (#51880) * Optionally enable asyncio debugging from debugpy integration * Unconditionally enable asyncio debugging --- homeassistant/components/debugpy/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 98f08827c23..72f2e8db067 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -1,7 +1,7 @@ """The Remote Python Debugger integration.""" from __future__ import annotations -from asyncio import Event +from asyncio import Event, get_event_loop import logging from threading import Thread @@ -15,8 +15,8 @@ from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType DOMAIN = "debugpy" -CONF_WAIT = "wait" CONF_START = "start" +CONF_WAIT = "wait" SERVICE_START = "start" CONFIG_SCHEMA = vol.Schema( @@ -43,7 +43,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def debug_start( call: ServiceCall | None = None, *, wait: bool = True ) -> None: - """Start the debugger.""" + """Enable asyncio debugging and start the debugger.""" + get_event_loop().set_debug(True) + debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) wait = conf[CONF_WAIT] From 333f199bd438c7e60b4cc79b394b04bda1151a75 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 15 Jun 2021 12:13:03 +0200 Subject: [PATCH 371/750] Additional units for HM-ES-TX-WM with ES-IEC (#50713) The Homematic HM-ES-TX-WM with ES-IEC adapter needs additional unit cast for `IEC_POWER` and `IEC_ENERGY_COUNTER`. --- homeassistant/components/homematic/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 964ba15cd0a..62f2f0ccdff 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -52,6 +52,8 @@ HM_UNIT_HA_CAST = { "ENERGY_COUNTER": ENERGY_WATT_HOUR, "GAS_POWER": VOLUME_CUBIC_METERS, "GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, + "IEC_POWER": POWER_WATT, + "IEC_ENERGY_COUNTER": ENERGY_WATT_HOUR, "LUX": LIGHT_LUX, "ILLUMINATION": LIGHT_LUX, "CURRENT_ILLUMINATION": LIGHT_LUX, From c0d311473cfa7c7524dba04dba01cf0920f6feb7 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 15 Jun 2021 13:08:19 +0200 Subject: [PATCH 372/750] Restore state of KNX Switch (#51761) --- homeassistant/components/knx/switch.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index c6fbb32b15d..b30313a047a 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -7,9 +7,10 @@ from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN, KNX_ADDRESS @@ -37,7 +38,7 @@ async def async_setup_platform( async_add_entities(entities) -class KNXSwitch(KnxEntity, SwitchEntity): +class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): """Representation of a KNX switch.""" def __init__(self, xknx: XKNX, config: ConfigType) -> None: @@ -54,6 +55,15 @@ class KNXSwitch(KnxEntity, SwitchEntity): ) self._unique_id = f"{self._device.switch.group_address}" + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if not self._device.switch.readable and ( + last_state := await self.async_get_last_state() + ): + if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._device.switch.value = last_state.state == STATE_ON + @property def is_on(self) -> bool: """Return true if device is on.""" From 515bd18ddd72d4c857fd13432767a1baa02749de Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 15 Jun 2021 04:19:48 -0700 Subject: [PATCH 373/750] Don't create unsupported pump sensors (#51828) * Don't create unsupported pump sensors * Remove old code and simplify new statements. * Address notes --- homeassistant/components/screenlogic/sensor.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 2419ee46eed..1ad18298655 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -68,11 +68,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Pump sensors for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items(): if pump_data["data"] != 0 and "currentWatts" in pump_data: - entities.extend( - ScreenLogicPumpSensor(coordinator, pump_num, pump_key) - for pump_key in pump_data - if pump_key in SUPPORTED_PUMP_SENSORS - ) + for pump_key in pump_data: + # Considerations for Intelliflow VF + if pump_data["pumpType"] == 1 and pump_key == "currentRPM": + continue + # Considerations for Intelliflow VS + if pump_data["pumpType"] == 2 and pump_key == "currentGPM": + continue + if pump_key in SUPPORTED_PUMP_SENSORS: + entities.append( + ScreenLogicPumpSensor(coordinator, pump_num, pump_key) + ) # IntelliChem sensors if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM: From 22b8dc16c206749095c0bbcdbcc633e7cc1b21fd Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:23:32 +0200 Subject: [PATCH 374/750] Add services to ezviz integration (#48984) Co-authored-by: Franck Nijhof --- homeassistant/components/ezviz/__init__.py | 3 +- homeassistant/components/ezviz/camera.py | 127 +++++++++++++++++- homeassistant/components/ezviz/config_flow.py | 11 +- homeassistant/components/ezviz/const.py | 35 ++--- homeassistant/components/ezviz/coordinator.py | 2 +- homeassistant/components/ezviz/manifest.json | 2 +- homeassistant/components/ezviz/services.yaml | 111 +++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ezviz/test_config_flow.py | 9 +- 10 files changed, 274 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/ezviz/services.yaml diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 670e07a07dc..19dd5121d69 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -2,7 +2,8 @@ from datetime import timedelta import logging -from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError +from pyezviz.client import EzvizClient +from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError from homeassistant.const import ( CONF_PASSWORD, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 919ff5039b2..d2dbd1b6aab 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,29 +1,43 @@ """Support ezviz camera devices.""" import asyncio -from datetime import timedelta import logging from haffmpeg.tools import IMAGE_JPEG, ImageFrame +from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + ATTR_DIRECTION, + ATTR_ENABLE, + ATTR_LEVEL, ATTR_SERIAL, + ATTR_SPEED, + ATTR_TYPE, CONF_CAMERAS, CONF_FFMPEG_ARGUMENTS, DATA_COORDINATOR, DEFAULT_CAMERA_USERNAME, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_RTSP_PORT, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_UP, DOMAIN, MANUFACTURER, + SERVICE_ALARM_SOUND, + SERVICE_ALARM_TRIGER, + SERVICE_DETECTION_SENSITIVITY, + SERVICE_PTZ, + SERVICE_WAKE_DEVICE, ) CAMERA_SCHEMA = vol.Schema( @@ -40,8 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Ezviz IP Camera from platform config.""" @@ -157,6 +169,46 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(camera_entities) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_PTZ, + { + vol.Required(ATTR_DIRECTION): vol.In( + [DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT] + ), + vol.Required(ATTR_SPEED): cv.positive_int, + }, + "perform_ptz", + ) + + platform.async_register_entity_service( + SERVICE_ALARM_TRIGER, + { + vol.Required(ATTR_ENABLE): cv.positive_int, + }, + "perform_sound_alarm", + ) + + platform.async_register_entity_service( + SERVICE_WAKE_DEVICE, {}, "perform_wake_device" + ) + + platform.async_register_entity_service( + SERVICE_ALARM_SOUND, + {vol.Required(ATTR_LEVEL): cv.positive_int}, + "perform_alarm_sound", + ) + + platform.async_register_entity_service( + SERVICE_DETECTION_SENSITIVITY, + { + vol.Required(ATTR_LEVEL): cv.positive_int, + vol.Required(ATTR_TYPE): cv.positive_int, + }, + "perform_set_alarm_detection_sensibility", + ) + class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): """An implementation of a Ezviz security camera.""" @@ -232,6 +284,24 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): """Camera Motion Detection Status.""" return self.coordinator.data[self._idx]["alarm_notify"] + def enable_motion_detection(self): + """Enable motion detection in camera.""" + try: + self.coordinator.ezviz_client.set_camera_defence(self._serial, 1) + + except InvalidHost as err: + _LOGGER.error("Error enabling motion detection") + raise InvalidHost from err + + def disable_motion_detection(self): + """Disable motion detection.""" + try: + self.coordinator.ezviz_client.set_camera_defence(self._serial, 0) + + except InvalidHost as err: + _LOGGER.error("Error disabling motion detection") + raise InvalidHost from err + @property def unique_id(self): """Return the name of this camera.""" @@ -271,3 +341,52 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): self._rtsp_stream = rtsp_stream_source return rtsp_stream_source return None + + def perform_ptz(self, direction, speed): + """Perform a PTZ action on the camera.""" + _LOGGER.debug("PTZ action '%s' on %s", direction, self._name) + try: + self.coordinator.ezviz_client.ptz_control( + str(direction).upper(), self._serial, "START", speed + ) + self.coordinator.ezviz_client.ptz_control( + str(direction).upper(), self._serial, "STOP", speed + ) + + except HTTPError as err: + _LOGGER.error("Cannot perform PTZ") + raise HTTPError from err + + def perform_sound_alarm(self, enable): + """Sound the alarm on a camera.""" + try: + self.coordinator.ezviz_client.sound_alarm(self._serial, enable) + except HTTPError as err: + _LOGGER.debug("Cannot sound alarm") + raise HTTPError from err + + def perform_wake_device(self): + """Basically wakes the camera by querying the device.""" + try: + self.coordinator.ezviz_client.get_detection_sensibility(self._serial) + except (HTTPError, PyEzvizError) as err: + _LOGGER.error("Cannot wake device") + raise PyEzvizError from err + + def perform_alarm_sound(self, level): + """Enable/Disable movement sound alarm.""" + try: + self.coordinator.ezviz_client.alarm_sound(self._serial, level, 1) + except HTTPError as err: + _LOGGER.error("Cannot set alarm sound level for on movement detected") + raise HTTPError from err + + def perform_set_alarm_detection_sensibility(self, level, type_value): + """Set camera detection sensibility level service.""" + try: + self.coordinator.ezviz_client.detection_sensibility( + self._serial, level, type_value + ) + except (HTTPError, PyEzvizError) as err: + _LOGGER.error("Cannot set detection sensitivity level") + raise PyEzvizError from err diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 6915d8fa8db..8f10e3f0698 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -1,8 +1,15 @@ """Config flow for ezviz.""" import logging -from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError -from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost, TestRTSPAuth +from pyezviz.client import EzvizClient +from pyezviz.exceptions import ( + AuthTestResultFailed, + HTTPError, + InvalidHost, + InvalidURL, + PyEzvizError, +) +from pyezviz.test_cam_rtsp import TestRTSPAuth import voluptuous as vol from homeassistant.config_entries import ConfigFlow, OptionsFlow diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index c307f0693f6..e3e2cae712c 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -6,29 +6,30 @@ MANUFACTURER = "Ezviz" # Configuration ATTR_SERIAL = "serial" CONF_CAMERAS = "cameras" -ATTR_SWITCH = "switch" -ATTR_ENABLE = "enable" -ATTR_DIRECTION = "direction" -ATTR_SPEED = "speed" -ATTR_LEVEL = "level" -ATTR_TYPE = "type_value" -DIR_UP = "up" -DIR_DOWN = "down" -DIR_LEFT = "left" -DIR_RIGHT = "right" -ATTR_LIGHT = "LIGHT" -ATTR_SOUND = "SOUND" -ATTR_INFRARED_LIGHT = "INFRARED_LIGHT" -ATTR_PRIVACY = "PRIVACY" -ATTR_SLEEP = "SLEEP" -ATTR_MOBILE_TRACKING = "MOBILE_TRACKING" -ATTR_TRACKING = "TRACKING" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" ATTR_HOME = "HOME_MODE" ATTR_AWAY = "AWAY_MODE" ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT" ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT" +# Services data +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" +ATTR_ENABLE = "enable" +ATTR_DIRECTION = "direction" +ATTR_SPEED = "speed" +ATTR_LEVEL = "level" +ATTR_TYPE = "type_value" + +# Service names +SERVICE_PTZ = "ptz" +SERVICE_ALARM_TRIGER = "sound_alarm" +SERVICE_WAKE_DEVICE = "wake_device" +SERVICE_ALARM_SOUND = "alarm_sound" +SERVICE_DETECTION_SENSITIVITY = "set_alarm_detection_sensibility" + # Defaults EU_URL = "apiieu.ezvizlife.com" RUSSIA_URL = "apirus.ezvizru.com" diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index 2fc9f6c9f82..ad755edce12 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from async_timeout import timeout -from pyezviz.client import HTTPError, InvalidURL, PyEzvizError +from pyezviz.exceptions import HTTPError, InvalidURL, PyEzvizError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 46abf8bc99a..d5a38b17755 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], - "requirements": ["pyezviz==0.1.8.7"], + "requirements": ["pyezviz==0.1.8.9"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/services.yaml b/homeassistant/components/ezviz/services.yaml new file mode 100644 index 00000000000..2635662e636 --- /dev/null +++ b/homeassistant/components/ezviz/services.yaml @@ -0,0 +1,111 @@ +alarm_sound: + name: Set warning sound level. + description: Set movement warning sound level. + target: + entity: + integration: ezviz + domain: camera + fields: + level: + name: Sound level + description: Sound level (2 is disabled, 1 intensive, 0 soft). + required: true + example: 0 + default: 0 + selector: + number: + min: 0 + max: 2 + step: 1 + mode: box +ptz: + name: PTZ + description: Moves the camera to the direction, with defined speed + target: + entity: + integration: ezviz + domain: camera + fields: + direction: + name: Direction + description: Direction to move camera (up, down, left, right). + required: true + example: "up" + default: "up" + selector: + select: + options: + - "up" + - "down" + - "left" + - "right" + speed: + name: Speed + description: Speed of movement (from 1 to 9). + required: true + example: 5 + default: 5 + selector: + number: + min: 1 + max: 9 + step: 1 + mode: box +set_alarm_detection_sensibility: + name: Detection sensitivity + description: Sets the detection sensibility level. + target: + entity: + integration: ezviz + domain: camera + fields: + level: + name: Sensitivity Level + description: 'Sensibility level (1-6) for type 0 (Normal camera) + or (1-100) for type 3 (PIR sensor camera).' + required: true + example: 3 + default: 3 + selector: + number: + min: 1 + max: 100 + step: 1 + mode: box + type_value: + name: Detection type + description: 'Type of detection. Options : 0 - Camera or 3 - PIR Sensor Camera' + required: true + example: '0' + default: '0' + selector: + select: + options: + - '0' + - '3' +sound_alarm: + name: Sound Alarm + description: Sounds the alarm on your camera. + target: + entity: + integration: ezviz + domain: camera + fields: + enable: + description: Enter 1 or 2 (1=disable, 2=enable). + required: true + example: 1 + default: 1 + selector: + number: + min: 1 + max: 2 + step: 1 + mode: box +wake_device: + name: Wake Camera + description: This can be used to wake the camera/device from hibernation. + target: + entity: + integration: ezviz + domain: camera diff --git a/requirements_all.txt b/requirements_all.txt index 03a5fff0f60..01b1a6109d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ pyephember==0.3.1 pyeverlights==0.1.0 # homeassistant.components.ezviz -pyezviz==0.1.8.7 +pyezviz==0.1.8.9 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0ba017a98f..b5b3a65e67e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -776,7 +776,7 @@ pyeconet==0.1.14 pyeverlights==0.1.0 # homeassistant.components.ezviz -pyezviz==0.1.8.7 +pyezviz==0.1.8.9 # homeassistant.components.fido pyfido==2.1.1 diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index b762f10447f..2775781cf00 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -2,8 +2,13 @@ from unittest.mock import patch -from pyezviz.client import HTTPError, InvalidURL, PyEzvizError -from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost +from pyezviz.exceptions import ( + AuthTestResultFailed, + HTTPError, + InvalidHost, + InvalidURL, + PyEzvizError, +) from homeassistant.components.ezviz.const import ( ATTR_SERIAL, From e4202aa4de50bab8b0a7e6736e27a5caf4f5c2b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Jun 2021 15:04:07 +0200 Subject: [PATCH 375/750] Upgrade pytest-cov to 2.12.1 (#51886) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 142bc879806..e9732ad7769 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,7 +14,7 @@ pylint==2.8.3 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 -pytest-cov==2.10.1 +pytest-cov==2.12.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 From 16d5d7e508540b766469c426807cfe0a2ac85c7a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Jun 2021 15:04:48 +0200 Subject: [PATCH 376/750] Upgrade codecov to 2.1.11 (#51885) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e9732ad7769..25a542d3ffb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -codecov==2.1.10 +codecov==2.1.11 coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 From b08f473da47bc717ab04705655aa2590a42a8207 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 15 Jun 2021 17:51:16 +0200 Subject: [PATCH 377/750] Add current hvac_action to KNX climate (#51464) --- homeassistant/components/knx/climate.py | 18 +++++++++++++++++- homeassistant/components/knx/const.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index b8c07767005..298ae391fe4 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -10,6 +10,8 @@ from xknx.telegram.address import parse_device_group_address from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, @@ -22,7 +24,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES +from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DOMAIN, PRESET_MODES from .knx_entity import KnxEntity from .schema import ClimateSchema @@ -260,6 +262,20 @@ class KNXClimate(KnxEntity, ClimateEntity): # default to ["heat"] return hvac_modes if hvac_modes else [HVAC_MODE_HEAT] + @property + def hvac_action(self) -> str | None: + """Return the current running hvac operation if supported. + + Need to be one of CURRENT_HVAC_*. + """ + if self._device.supports_on_off and not self._device.is_on: + return CURRENT_HVAC_OFF + if self._device.mode is not None and self._device.mode.supports_controller_mode: + return CURRENT_HVAC_ACTIONS.get( + self._device.mode.controller_mode.value, CURRENT_HVAC_IDLE + ) + return None + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set operation mode.""" if self._device.supports_on_off and hvac_mode == HVAC_MODE_OFF: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 4935cbd09d4..74c6045b767 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -3,6 +3,11 @@ from enum import Enum from typing import Final from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -68,6 +73,14 @@ CONTROLLER_MODES: Final = { "Dry": HVAC_MODE_DRY, } +CURRENT_HVAC_ACTIONS: Final = { + "Heat": CURRENT_HVAC_HEAT, + "Cool": CURRENT_HVAC_COOL, + "Off": CURRENT_HVAC_OFF, + "Fan only": CURRENT_HVAC_FAN, + "Dry": CURRENT_HVAC_DRY, +} + PRESET_MODES: Final = { # Map DPT 20.102 HVAC operating modes to HA presets "Auto": PRESET_NONE, From 37af42e470c7e2c6ca1188f18c3d1c3f64348987 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Jun 2021 19:14:57 +0200 Subject: [PATCH 378/750] Upgrade pillow to 8.2.0 (#51897) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 4e31ca03371..ae584af5916 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==8.1.2"], + "requirements": ["pydoods==1.0.2", "pillow==8.2.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 741fb8511a6..82b7e58a653 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==8.1.2"], + "requirements": ["pillow==8.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 86f3d23d308..68c7717e16c 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==8.1.2"], + "requirements": ["pillow==8.2.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 18bf2d7db6d..a414e197fd6 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"], + "requirements": ["pillow==8.2.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated" } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 7c4ea22497c..9a0287b2132 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==8.1.2"], + "requirements": ["pillow==8.2.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e372c995b5e..b22b645a7e8 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==8.1.2", "simplehound==0.3"], + "requirements": ["pillow==8.2.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 74c10af363d..1b161b4aec8 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.20.3", - "pillow==8.1.2" + "pillow==8.2.0" ], "codeowners": [], "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 60ae7617702..8e611e05502 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 paho-mqtt==1.5.1 -pillow==8.1.2 +pillow==8.2.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 pyyaml==5.4.1 diff --git a/requirements_all.txt b/requirements_all.txt index 01b1a6109d8..bdd6d1363ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1160,7 +1160,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.1.2 +pillow==8.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5b3a65e67e..db8d54224c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -636,7 +636,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==8.1.2 +pillow==8.2.0 # homeassistant.components.plex plexapi==4.5.1 From 3488b7836521f9d5963b77cff5614cec9f92968c Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 15 Jun 2021 10:17:10 -0700 Subject: [PATCH 379/750] Add a menu_cursor service to the yamaha component (#44819) * Add a menu_cursor service to the yamaha component * Update homeassistant/components/yamaha/media_player.py Co-authored-by: Martin Hjelmare * Update service description to new format Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- homeassistant/components/yamaha/const.py | 7 +++++ .../components/yamaha/media_player.py | 31 ++++++++++++++++++- homeassistant/components/yamaha/services.yaml | 14 +++++++++ tests/components/yamaha/test_media_player.py | 26 ++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index fea962938eb..bcfdc55a511 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -1,4 +1,11 @@ """Constants for the Yamaha component.""" DOMAIN = "yamaha" +CURSOR_TYPE_DOWN = "down" +CURSOR_TYPE_LEFT = "left" +CURSOR_TYPE_RETURN = "return" +CURSOR_TYPE_RIGHT = "right" +CURSOR_TYPE_SELECT = "select" +CURSOR_TYPE_UP = "up" SERVICE_ENABLE_OUTPUT = "enable_output" +SERVICE_MENU_CURSOR = "menu_cursor" SERVICE_SELECT_SCENE = "select_scene" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 3f79be43f6e..147a983b298 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -31,10 +31,21 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv, entity_platform -from .const import SERVICE_ENABLE_OUTPUT, SERVICE_SELECT_SCENE +from .const import ( + CURSOR_TYPE_DOWN, + CURSOR_TYPE_LEFT, + CURSOR_TYPE_RETURN, + CURSOR_TYPE_RIGHT, + CURSOR_TYPE_SELECT, + CURSOR_TYPE_UP, + SERVICE_ENABLE_OUTPUT, + SERVICE_MENU_CURSOR, + SERVICE_SELECT_SCENE, +) _LOGGER = logging.getLogger(__name__) +ATTR_CURSOR = "cursor" ATTR_ENABLED = "enabled" ATTR_PORT = "port" @@ -45,6 +56,14 @@ CONF_SOURCE_NAMES = "source_names" CONF_ZONE_IGNORE = "zone_ignore" CONF_ZONE_NAMES = "zone_names" +CURSOR_TYPE_MAP = { + CURSOR_TYPE_DOWN: rxv.RXV.menu_down.__name__, + CURSOR_TYPE_LEFT: rxv.RXV.menu_left.__name__, + CURSOR_TYPE_RETURN: rxv.RXV.menu_return.__name__, + CURSOR_TYPE_RIGHT: rxv.RXV.menu_right.__name__, + CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__, + CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__, +} DATA_YAMAHA = "yamaha_known_receivers" DEFAULT_NAME = "Yamaha Receiver" @@ -164,6 +183,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= {vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string}, "enable_output", ) + # Register Service 'menu_cursor' + platform.async_register_entity_service( + SERVICE_MENU_CURSOR, + {vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)}, + YamahaDevice.menu_cursor.__name__, + ) class YamahaDevice(MediaPlayerEntity): @@ -382,6 +407,10 @@ class YamahaDevice(MediaPlayerEntity): """Enable or disable an output port..""" self.receiver.enable_output(port, enabled) + def menu_cursor(self, cursor): + """Press a menu cursor button.""" + getattr(self.receiver, CURSOR_TYPE_MAP[cursor])() + def set_scene(self, scene): """Set the current scene.""" try: diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index fe2b2c66384..8d25d5925c1 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -19,6 +19,20 @@ enable_output: required: true selector: boolean: +menu_cursor: + name: Menu cursor + description: Control the cursor in a menu + target: + entity: + integration: yamaha + domain: media_player + fields: + cursor: + name: Cursor + description: Name of the cursor key to press ('up', 'down', 'left', 'right', 'select', 'return') + example: down + selector: + text: select_scene: name: Select scene description: "Select a scene on the receiver" diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 84a9e475c32..45624ae0a8b 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -130,6 +130,32 @@ async def test_enable_output(hass, device, main_zone): assert main_zone.enable_output.call_args == call(port, enabled) +@pytest.mark.parametrize( + "cursor,method", + [ + (yamaha.CURSOR_TYPE_DOWN, "menu_down"), + (yamaha.CURSOR_TYPE_LEFT, "menu_left"), + (yamaha.CURSOR_TYPE_RETURN, "menu_return"), + (yamaha.CURSOR_TYPE_RIGHT, "menu_right"), + (yamaha.CURSOR_TYPE_SELECT, "menu_sel"), + (yamaha.CURSOR_TYPE_UP, "menu_up"), + ], +) +@pytest.mark.usefixtures("device") +async def test_menu_cursor(hass, main_zone, cursor, method): + """Verify that the correct menu method is called for the menu_cursor service.""" + assert await async_setup_component(hass, mp.DOMAIN, CONFIG) + await hass.async_block_till_done() + + data = { + "entity_id": "media_player.yamaha_receiver_main_zone", + "cursor": cursor, + } + await hass.services.async_call(DOMAIN, yamaha.SERVICE_MENU_CURSOR, data, True) + + getattr(main_zone, method).assert_called_once_with() + + async def test_select_scene(hass, device, main_zone, caplog): """Test select scene service.""" scene_prop = PropertyMock(return_value=None) From 63e20f2ced843da64319b468a429b3553abfbe6c Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 15 Jun 2021 19:21:30 +0200 Subject: [PATCH 380/750] Mark config flow fields as required (#51898) * flo * goalzero * mutesync * ring * roon * risco * Ruckus Unleashed * Scaffold template --- homeassistant/components/flo/config_flow.py | 2 +- homeassistant/components/goalzero/config_flow.py | 2 +- homeassistant/components/mutesync/config_flow.py | 2 +- homeassistant/components/ring/config_flow.py | 6 ++++-- homeassistant/components/risco/config_flow.py | 8 +++++++- homeassistant/components/roon/config_flow.py | 2 +- homeassistant/components/ruckus_unleashed/config_flow.py | 8 +++++++- .../templates/config_flow/integration/config_flow.py | 8 +++++++- 8 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index 038fc33777a..306ec945a3e 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER -DATA_SCHEMA = vol.Schema({"username": str, "password": str}) +DATA_SCHEMA = vol.Schema({vol.Required("username"): str, vol.Required("password"): str}) async def validate_input(hass: core.HomeAssistant, data): diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 575ff2ba350..cea47c967a8 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -18,7 +18,7 @@ from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({"host": str, "name": str}) +DATA_SCHEMA = vol.Schema({vol.Required("host"): str, vol.Required("name"): str}) class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index e4964d552b0..99412ed1795 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN -STEP_USER_DATA_SCHEMA = vol.Schema({"host": str}) +STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index d4cc6796bf1..cca0c231d96 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -64,7 +64,9 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=vol.Schema({"username": str, "password": str}), + data_schema=vol.Schema( + {vol.Required("username"): str, vol.Required("password"): str} + ), errors=errors, ) @@ -75,7 +77,7 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="2fa", - data_schema=vol.Schema({"2fa": str}), + data_schema=vol.Schema({vol.Required("2fa"): str}), ) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 0bc9c49707a..c20aa2af287 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -30,7 +30,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str, CONF_PIN: str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PIN): str, + } +) HA_STATES = [ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index 799d50bdaab..31391a0ff36 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -18,7 +18,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({"host": str}) +DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) TIMEOUT = 120 diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 463c7b1d550..7d34e620a13 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -12,7 +12,13 @@ from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN _LOGGER = logging.getLogger(__package__) -DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) def validate_input(hass: core.HomeAssistant, data): diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index f88390599e7..cf6dfd9cc20 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -16,7 +16,13 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) # TODO adjust the data schema to the data that you need -STEP_USER_DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) class PlaceholderHub: From 6d656dd2ecc5a7dcdc1d580358f8d0900f27cd27 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 16 Jun 2021 02:27:53 +0800 Subject: [PATCH 381/750] Speed up record stream audio test (#51901) --- tests/components/stream/common.py | 92 +++++++++++++++--------- tests/components/stream/test_recorder.py | 13 ++-- 2 files changed, 66 insertions(+), 39 deletions(-) diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 4c6841d03db..a39e8bdca21 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -8,26 +8,27 @@ import numpy as np AUDIO_SAMPLE_RATE = 8000 -def generate_h264_video(container_format="mp4", audio_codec=None): +def generate_audio_frame(pcm_mulaw=False): + """Generate a blank audio frame.""" + if pcm_mulaw: + audio_frame = av.AudioFrame(format="s16", layout="mono", samples=1) + audio_bytes = b"\x00\x00" + else: + audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024) + audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024 + audio_frame.planes[0].update(audio_bytes) + audio_frame.sample_rate = AUDIO_SAMPLE_RATE + audio_frame.time_base = Fraction(1, AUDIO_SAMPLE_RATE) + return audio_frame + + +def generate_h264_video(container_format="mp4"): """ Generate a test video. See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html """ - def generate_audio_frame(pcm_mulaw=False): - """Generate a blank audio frame.""" - if pcm_mulaw: - audio_frame = av.AudioFrame(format="s16", layout="mono", samples=1) - audio_bytes = b"\x00\x00" - else: - audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024) - audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024 - audio_frame.planes[0].update(audio_bytes) - audio_frame.sample_rate = AUDIO_SAMPLE_RATE - audio_frame.time_base = Fraction(1, AUDIO_SAMPLE_RATE) - return audio_frame - duration = 5 fps = 24 total_frames = duration * fps @@ -42,6 +43,39 @@ def generate_h264_video(container_format="mp4", audio_codec=None): stream.pix_fmt = "yuv420p" stream.options.update({"g": str(fps), "keyint_min": str(fps)}) + for frame_i in range(total_frames): + + img = np.empty((480, 320, 3)) + img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) + img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) + img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) + + img = np.round(255 * img).astype(np.uint8) + img = np.clip(img, 0, 255) + + frame = av.VideoFrame.from_ndarray(img, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + + # Flush stream + for packet in stream.encode(): + container.mux(packet) + + # Close the file + container.close() + output.seek(0) + + return output + + +def remux_with_audio(source, container_format, audio_codec): + """Remux an existing source with new audio.""" + av_source = av.open(source, mode="r") + output = io.BytesIO() + output.name = "test.mov" if container_format == "mov" else "test.mp4" + container = av.open(output, mode="w", format=container_format) + container.add_stream(template=av_source.streams.video[0]) + a_packet = None last_a_dts = -1 if audio_codec is not None: @@ -57,23 +91,17 @@ def generate_h264_video(container_format="mp4", audio_codec=None): if a_packets: a_packet = a_packets[0] - for frame_i in range(total_frames): - - img = np.empty((480, 320, 3)) - img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) - img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) - img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) - - img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) - - frame = av.VideoFrame.from_ndarray(img, format="rgb24") - for packet in stream.encode(frame): - container.mux(packet) - + # open original source and iterate through video packets + for packet in av_source.demux(video=0): + if not packet.dts: + continue + container.mux(packet) if a_packet is not None: - a_packet.pts = int(frame_i / (fps * a_packet.time_base)) - while a_packet.pts * a_packet.time_base * fps < frame_i + 1: + a_packet.pts = int(packet.dts * packet.time_base / a_packet.time_base) + while ( + a_packet.pts * a_packet.time_base + < (packet.dts + packet.duration) * packet.time_base + ): a_packet.dts = a_packet.pts if ( a_packet.dts > last_a_dts @@ -82,10 +110,6 @@ def generate_h264_video(container_format="mp4", audio_codec=None): last_a_dts = a_packet.dts a_packet.pts += a_packet.duration - # Flush stream - for packet in stream.encode(): - container.mux(packet) - # Close the file container.close() output.seek(0) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 72c6dfa197f..31661db3886 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from tests.components.stream.common import generate_h264_video +from tests.components.stream.common import generate_h264_video, remux_with_audio MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever @@ -190,19 +190,22 @@ async def test_record_stream_audio( """ await async_setup_component(hass, "stream", {"stream": {}}) + # Generate source video with no audio + source = generate_h264_video(container_format="mov") + for a_codec, expected_audio_streams in ( ("aac", 1), # aac is a valid mp4 codec ("pcm_mulaw", 0), # G.711 is not a valid mp4 codec ("empty", 0), # audio stream with no packets (None, 0), # no audio stream ): + + # Remux source video with new audio + source = remux_with_audio(source, "mov", a_codec) # mov can store PCM + record_worker_sync.reset() stream_worker_sync.pause() - # Setup demo track - source = generate_h264_video( - container_format="mov", audio_codec=a_codec - ) # mov can store PCM stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") From a072fb059d43af7be747c282d4c9dd84b8857543 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 16 Jun 2021 00:09:27 +0000 Subject: [PATCH 382/750] [ci skip] Translation update --- .../components/roomba/translations/nl.json | 4 +- .../xiaomi_miio/translations/ca.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/et.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/nl.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/no.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/pl.json | 16 ++++- .../xiaomi_miio/translations/ru.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/zh-Hant.json | 58 ++++++++++++++++++- .../yamaha_musiccast/translations/pl.json | 11 ++++ 9 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/yamaha_musiccast/translations/pl.json diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index a18bd89ae12..40c821b62db 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -45,8 +45,8 @@ "host": "Host", "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" + "description": "Kies een Roomba of Braava.", + "title": "Automatisch verbinding maken met het apparaat" } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 6ee0b1e16fd..ff0a24170f6 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "[%key::common::config_flow::abort::already_configured_device%]", - "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs" + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "incomplete_info": "Informaci\u00f3 incompleta per configurar el dispositiu, no s'ha proporcionat cap amfitri\u00f3 o token.", + "not_xiaomi_miio": "Xiaomi Miio encara no \u00e9s compatible amb el dispositiu.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds", + "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xioami Miio Cloud, comprova les credencials.", + "cloud_no_devices": "No s'han trobat dispositius en aquest compte al n\u00favol de Xiaomi Miio.", "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Pa\u00eds del servidor al n\u00favol", + "cloud_password": "Contrasenya del n\u00favol", + "cloud_username": "Nom d'usuari del n\u00favol", + "manual": "Configuraci\u00f3 manual (no recomanada)" + }, + "description": "Inicia sessi\u00f3 al n\u00favol Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers per obtenir el servidor al n\u00favol.", + "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + }, + "connect": { + "data": { + "model": "Model de dispositiu" + }, + "description": "Selecciona manualment el model de dispositiu entre els models compatibles.", + "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + }, "device": { "data": { "host": "Adre\u00e7a IP", @@ -30,6 +53,25 @@ "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", "title": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" }, + "manual": { + "data": { + "host": "Adre\u00e7a IP", + "token": "Token d'API" + }, + "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", + "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Xiaomi Miio ha de tornar a autenticar-se amb el teu compte per poder actualitzar els tokens o afegir credencials pel n\u00favol.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "select": { + "data": { + "select_device": "Dispositiu Miio" + }, + "description": "Selecciona el dispositiu Xiaomi Miio a configurar.", + "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + }, "user": { "data": { "gateway": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Utilitza el n\u00favol per obtenir subdispositius connectats" + }, + "description": "Especifica par\u00e0metres opcionals", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index acc03463883..92d8ffe048f 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "already_in_progress": "Seadistamine on juba k\u00e4imas" + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "incomplete_info": "Puudulik seadistusteave, hosti v\u00f5i p\u00e4\u00e4suluba pole esitatud.", + "not_xiaomi_miio": "Seade ei ole (veel) Xiaomi Miio poolt toetatud.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendus nurjus", + "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik", + "cloud_login_error": "Xioami Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", + "cloud_no_devices": "Xiaomi Miio pilvekontolt ei leitud \u00fchtegi seadet.", "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Pilveserveri riik", + "cloud_password": "Pilve salas\u00f5na", + "cloud_username": "Pilve kasutajatunnus", + "manual": "Seadista k\u00e4sitsi (pole soovitatav)" + }, + "description": "Logi sisse Xiaomi Miio pilve, vaata https://www.openhab.org/addons/bindings/miio/#country-servers pilveserveri kasutamiseks.", + "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Seadme mudel" + }, + "description": "Vali seadme mudel k\u00e4sitsi toetatud mudelite hulgast.", + "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + }, "device": { "data": { "host": "IP-aadress", @@ -30,6 +53,25 @@ "description": "On vaja 32-kohalist API-tokenti, juhiste saamiseks vaata lehte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see token erineb Xiaomi Aqara sidumisel kasutatavast v\u00f5tmest.", "title": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga" }, + "manual": { + "data": { + "host": "IP aadress", + "token": "API v\u00f5ti" + }, + "description": "On vajalik 32 t\u00e4hem\u00e4rki API v\u00f5ti, vt. https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token juhiseid. Pane t\u00e4hele, et see API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.", + "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "Xiaomi Miio sidumine peab konto uuesti tuvastama, et v\u00e4rskendada p\u00e4\u00e4sulube v\u00f5i lisada puuduv pilvemandaat.", + "title": "Taastuvastamine" + }, + "select": { + "data": { + "select_device": "Miio seade" + }, + "description": "Vali seadistamiseks Xiaomi Miio seade.", + "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + }, "user": { "data": { "gateway": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u00dchendatud alamseadmete hankimiseks kasuta pilve" + }, + "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", + "title": "Xiaomi Miio" + } + } } } \ 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 012b976bea3..08c64808a3b 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratiestroom is al aan de gang", + "incomplete_info": "Onvolledige informatie voor het instellen van het apparaat, geen host of token opgegeven.", + "not_xiaomi_miio": "Apparaat wordt (nog) niet ondersteund door Xiaomi Miio.", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", + "cloud_credentials_incomplete": "Cloud-inloggegevens onvolledig, vul gebruikersnaam, wachtwoord en land in", + "cloud_login_error": "Kan niet inloggen op Xioami Miio Cloud, controleer de inloggegevens.", + "cloud_no_devices": "Geen apparaten gevonden in dit Xiaomi Miio-cloudaccount.", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft", "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Land van cloudserver", + "cloud_password": "Cloud wachtwoord", + "cloud_username": "Cloud gebruikersnaam", + "manual": "Handmatig configureren (niet aanbevolen)" + }, + "description": "Log in op de Xiaomi Miio-cloud, zie https://www.openhab.org/addons/bindings/miio/#country-servers voor de te gebruiken cloudserver.", + "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Apparaatmodel" + }, + "description": "Selecteer handmatig het apparaatmodel uit de ondersteunde modellen.", + "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" + }, "device": { "data": { "host": "IP-adres", @@ -30,6 +53,25 @@ "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" }, + "manual": { + "data": { + "host": "IP-adres", + "token": "API-token" + }, + "description": "U hebt het 32-teken API-token , zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Houd er rekening mee dat deze API-token verschilt van de sleutel die wordt gebruikt door de Xiaomi Aqara-integratie.", + "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "De Xiaomi Miio-integratie moet uw account opnieuw verifi\u00ebren om de tokens bij te werken of ontbrekende cloudreferenties toe te voegen.", + "title": "Verifieer de integratie opnieuw" + }, + "select": { + "data": { + "select_device": "Miio-apparaat" + }, + "description": "Selecteer het Xiaomi Miio apparaat dat u wilt instellen.", + "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi-gateway" + }, "user": { "data": { "gateway": "Maak verbinding met een Xiaomi Gateway" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Cloud-inloggegevens onvolledig, vul gebruikersnaam, wachtwoord en land in" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Gebruik de cloud om aangesloten subapparaten te krijgen" + }, + "description": "Optionele instellingen opgeven", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index e5ca4d2d004..8fa93169647 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede" + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "incomplete_info": "Ufullstendig informasjon til installasjonsenheten, ingen vert eller token leveres.", + "not_xiaomi_miio": "Enheten st\u00f8ttes (enn\u00e5) ikke av Xiaomi Miio.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", + "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land", + "cloud_login_error": "Kunne ikke logge p\u00e5 Xioami Miio Cloud, sjekk legitimasjonen.", + "cloud_no_devices": "Ingen enheter funnet i denne Xiaomi Miio-skykontoen.", "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Land for skyserver", + "cloud_password": "Passord for sky", + "cloud_username": "Brukernavn i skyen", + "manual": "Konfigurer manuelt (anbefales ikke)" + }, + "description": "Logg deg p\u00e5 Xiaomi Miio-skyen, se https://www.openhab.org/addons/bindings/miio/#country-servers for skyserveren du kan bruke.", + "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Enhetsmodell" + }, + "description": "Velg enhetsmodellen manuelt fra de st\u00f8ttede modellene.", + "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + }, "device": { "data": { "host": "IP adresse", @@ -30,6 +53,25 @@ "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", "title": "Koble til en Xiaomi Gateway" }, + "manual": { + "data": { + "host": "IP adresse", + "token": "API-token" + }, + "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", + "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "Xiaomi Miio-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt for \u00e5 oppdatere tokens eller legge til manglende skylegitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "select": { + "data": { + "select_device": "Miio-enhet" + }, + "description": "Velg Xiaomi Miio-enheten du vil installere.", + "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + }, "user": { "data": { "gateway": "Koble til en Xiaomi Gateway" @@ -38,5 +80,19 @@ "title": "" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Bruk skyen for \u00e5 f\u00e5 tilkoblede underenheter" + }, + "description": "Spesifiser valgfrie innstillinger", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index a7c01ef346e..9051e9de3d9 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku" + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -30,6 +31,12 @@ "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara .", "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi" }, + "manual": { + "data": { + "host": "Adres IP", + "token": "Token API" + } + }, "user": { "data": { "gateway": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi" @@ -38,5 +45,12 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "step": { + "init": { + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index ef729f33a1e..f9aeb824b20 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f." + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "incomplete_info": "\u041d\u0435\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 \u0442\u043e\u043a\u0435\u043d.", + "not_xiaomi_miio": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u043f\u043e\u043a\u0430) \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f Xiaomi Miio.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443.", + "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xioami Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "cloud_no_devices": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "\u0421\u0442\u0440\u0430\u043d\u0430 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430", + "cloud_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430", + "cloud_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430", + "manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e (\u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)" + }, + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e Xiaomi Miio, \u0441\u043c. https://www.openhab.org/addons/bindings/miio/#country-servers \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "connect": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u0437 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, "device": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", @@ -30,6 +53,25 @@ "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Xiaomi" }, + "manual": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio, \u0447\u0442\u043e\u0431\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d\u044b \u0438\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043e\u0431\u043b\u0430\u043a\u0430.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "select": { + "data": { + "select_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Miio" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Miio \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, "user": { "data": { "gateway": "\u0428\u043b\u044e\u0437 Xiaomi" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443." + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u0430\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index c79f6906a45..fdbcde43114 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "incomplete_info": "\u6240\u63d0\u4f9b\u4e4b\u88dd\u7f6e\u8cc7\u8a0a\u4e0d\u5b8c\u6574\u3001\u7121\u4e3b\u6a5f\u7aef\u6216\u6b0a\u6756\uff0c\u7121\u6cd5\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "not_xiaomi_miio": "\u5c0f\u7c73 Miio \uff08\u5c1a\uff09\u4e0d\u652f\u63f4\u8a72\u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "cloud_credentials_incomplete": "\u96f2\u7aef\u6191\u8b49\u672a\u5b8c\u6210\uff0c\u8acb\u586b\u5beb\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u8207\u570b\u5bb6", + "cloud_login_error": "\u7121\u6cd5\u767b\u5165\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u6aa2\u67e5\u6191\u8b49\u3002", + "cloud_no_devices": "\u5c0f\u7c73 Miio \u96f2\u7aef\u5e33\u865f\u672a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\u3002", "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002", "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002" }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "\u96f2\u7aef\u670d\u52d9\u4f3a\u670d\u5668\u570b\u5bb6", + "cloud_password": "\u96f2\u7aef\u670d\u52d9\u5bc6\u78bc", + "cloud_username": "\u96f2\u7aef\u670d\u52d9\u4f7f\u7528\u8005\u540d\u7a31", + "manual": "\u624b\u52d5\u8a2d\u5b9a (\u4e0d\u5efa\u8b70)" + }, + "description": "\u767b\u5165\u81f3\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u53c3\u95b1 https://www.openhab.org/addons/bindings/miio/#country-servers \u4ee5\u4e86\u89e3\u9078\u64c7\u54ea\u4e00\u7d44\u96f2\u7aef\u4f3a\u670d\u5668\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + }, + "connect": { + "data": { + "model": "\u88dd\u7f6e\u578b\u865f" + }, + "description": "\u5f9e\u652f\u63f4\u7684\u578b\u865f\u4e2d\u624b\u52d5\u9078\u64c7\u88dd\u7f6e\u578b\u865f\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + }, "device": { "data": { "host": "IP \u4f4d\u5740", @@ -30,6 +53,25 @@ "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", "title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" }, + "manual": { + "data": { + "host": "IP \u4f4d\u5740", + "token": "API \u6b0a\u6756" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u5bc6\u9470\u4e0d\u540c\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + }, + "reauth_confirm": { + "description": "\u5c0f\u7c73 Miio \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f\u3001\u65b9\u80fd\u66f4\u65b0\u6b0a\u6756\u6216\u65b0\u589e\u907a\u5931\u7684\u96f2\u7aef\u6191\u8b49\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "select": { + "data": { + "select_device": "Miio \u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684 \u5c0f\u7c73 Miio \u88dd\u7f6e\u3002", + "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + }, "user": { "data": { "gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" @@ -38,5 +80,19 @@ "title": "\u5c0f\u7c73 Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "\u96f2\u7aef\u6191\u8b49\u672a\u5b8c\u6210\uff0c\u8acb\u586b\u5beb\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u8207\u570b\u5bb6" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u4f7f\u7528\u96f2\u7aef\u53d6\u5f97\u9023\u7dda\u5b50\u88dd\u7f6e" + }, + "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", + "title": "\u5c0f\u7c73 Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json new file mode 100644 index 00000000000..4d32dce32f9 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/pl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file From ff0c753c874c4b9f5c6c009873c74e38ac090273 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 16 Jun 2021 03:34:05 +0200 Subject: [PATCH 383/750] Bump pyRFXtrx to 0.27.0 (#51911) * Bump version * Fix test --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_init.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 34c31c72a0d..0fc12e79d49 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.26.1"], + "requirements": ["pyRFXtrx==0.27.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index bdd6d1363ca..75937cd3439 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1264,7 +1264,7 @@ pyMetEireann==0.2 pyMetno==0.8.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.26.1 +pyRFXtrx==0.27.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db8d54224c9..47bd108f6ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -704,7 +704,7 @@ pyMetEireann==0.2 pyMetno==0.8.3 # homeassistant.components.rfxtrx -pyRFXtrx==0.26.1 +pyRFXtrx==0.27.0 # homeassistant.components.tibber pyTibber==0.17.0 diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index b3829e2b5cc..3625c23ebb8 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -117,7 +117,7 @@ async def test_fire_event(hass, rfxtrx): "type_string": "Byron SX", "id_string": "00:90", "data": "0716000100900970", - "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9}, + "values": {"Command": "Sound 9", "Rssi numeric": 7, "Sound": 9}, "device_id": device_id_2.id, }, ] From 37d3a4dd53dd64280ddd2beffb616350e27d6ad3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 08:29:31 +0200 Subject: [PATCH 384/750] Use entity class vars in Switch demo (#51906) * Use entity class vars in Switch demo * Fix missing initial state of the demo switch --- homeassistant/components/demo/switch.py | 88 ++++++++----------------- 1 file changed, 27 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index cdbeb142677..13853959a12 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,4 +1,6 @@ """Demo platform that has two fake switches.""" +from __future__ import annotations + from homeassistant.components.switch import SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME @@ -9,9 +11,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the demo switches.""" async_add_entities( [ - DemoSwitch("swith1", "Decorative Lights", True, None, True), + DemoSwitch("switch1", "Decorative Lights", True, None, True), DemoSwitch( - "swith2", + "switch2", "AC", False, "mdi:air-conditioner", @@ -30,78 +32,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoSwitch(SwitchEntity): """Representation of a demo switch.""" - def __init__(self, unique_id, name, state, icon, assumed, device_class=None): + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: bool, + icon: str | None, + assumed: bool, + device_class: str | None = None, + ) -> None: """Initialize the Demo switch.""" - self._unique_id = unique_id - self._name = name or DEVICE_DEFAULT_NAME - self._state = state - self._icon = icon - self._assumed = assumed - self._device_class = device_class + self._attr_assumed_state = assumed + self._attr_device_class = device_class + self._attr_icon = icon + self._attr_is_on = state + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_today_energy_kwh = 15 + self._attr_unique_id = unique_id @property def device_info(self): """Return device info.""" return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.unique_id) - }, + "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, } - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """No polling needed for a demo switch.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def assumed_state(self): - """Return if the state is based on assumptions.""" - return self._assumed - - @property - def current_power_w(self): - """Return the current power usage in W.""" - if self._state: - return 100 - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return 15 - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - @property - def device_class(self): - """Return device of entity.""" - return self._device_class - def turn_on(self, **kwargs): """Turn the switch on.""" - self._state = True + self._attr_is_on = True + self._attr_current_power_w = 100 self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - self._state = False + self._attr_is_on = False + self._attr_current_power_w = 0 self.schedule_update_ha_state() From c65d1206337275a84d885da6665cebda8eb6af29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 09:58:21 +0200 Subject: [PATCH 385/750] Fix typo in min/max mired(s) entity class attribute (#51921) --- homeassistant/components/elgato/light.py | 4 ++-- homeassistant/components/light/__init__.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 46060fe23fb..7f7e432c5f0 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -77,8 +77,8 @@ class ElgatoLight(LightEntity): min_mired = 153 max_mired = 285 - self._attr_max_mired = max_mired - self._attr_min_mired = min_mired + self._attr_max_mireds = max_mired + self._attr_min_mireds = min_mired self._attr_name = info.display_name or info.product_name self._attr_supported_color_modes = supported_color_modes self._attr_unique_id = info.serial_number diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index ce86ae4e257..e92999f4d21 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -647,8 +647,8 @@ class LightEntity(ToggleEntity): _attr_effect_list: list[str] | None = None _attr_effect: str | None = None _attr_hs_color: tuple[float, float] | None = None - _attr_max_mired: int = 500 - _attr_min_mired: int = 153 + _attr_max_mireds: int = 500 + _attr_min_mireds: int = 153 _attr_rgb_color: tuple[int, int, int] | None = None _attr_rgbw_color: tuple[int, int, int, int] | None = None _attr_rgbww_color: tuple[int, int, int, int, int] | None = None @@ -748,14 +748,14 @@ class LightEntity(ToggleEntity): """Return the coldest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts - return self._attr_min_mired + return self._attr_min_mireds @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts - return self._attr_max_mired + return self._attr_max_mireds @property def white_value(self) -> int | None: From 61079ab7fa9046dffadc95c8cbd300e2fe0cd647 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 16 Jun 2021 03:00:34 -0700 Subject: [PATCH 386/750] Support receiving long-press events from WeMo devices (#45503) Co-authored-by: Martin Hjelmare --- homeassistant/components/wemo/__init__.py | 25 +++-- homeassistant/components/wemo/const.py | 3 + .../components/wemo/device_trigger.py | 61 ++++++++++++ homeassistant/components/wemo/entity.py | 53 +++++----- homeassistant/components/wemo/light.py | 4 +- homeassistant/components/wemo/strings.json | 5 + homeassistant/components/wemo/wemo_device.py | 96 ++++++++++++++++++ tests/components/wemo/conftest.py | 4 +- tests/components/wemo/entity_test_helpers.py | 23 ++++- tests/components/wemo/test_device_trigger.py | 98 +++++++++++++++++++ tests/components/wemo/test_init.py | 1 + tests/components/wemo/test_wemo_device.py | 40 ++++++++ 12 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/wemo/device_trigger.py create mode 100644 homeassistant/components/wemo/wemo_device.py create mode 100644 tests/components/wemo/test_device_trigger.py create mode 100644 tests/components/wemo/test_wemo_device.py diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index dfdd0a0adb6..9e9aa5ee278 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN +from .wemo_device import async_register_device # Max number of devices to initialize at once. This limit is in place to # avoid tying up too many executor threads with WeMo device setup. @@ -105,6 +106,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Keep track of WeMo device subscriptions for push updates registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) + + # Respond to discovery requests from WeMo devices. + discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) + await hass.async_add_executor_job(discovery_responder.start) + static_conf = config.get(CONF_STATIC, []) wemo_dispatcher = WemoDispatcher(entry) wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) @@ -113,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") await hass.async_add_executor_job(registry.stop) + await hass.async_add_executor_job(discovery_responder.stop) wemo_discovery.async_stop_discovery() entry.async_on_unload( @@ -137,15 +144,15 @@ class WemoDispatcher: self._added_serial_numbers = set() self._loaded_components = set() - @callback - def async_add_unique_device( - self, hass: HomeAssistant, device: pywemo.WeMoDevice + async def async_add_unique_device( + self, hass: HomeAssistant, wemo: pywemo.WeMoDevice ) -> None: """Add a WeMo device to hass if it has not already been added.""" - if device.serialnumber in self._added_serial_numbers: + if wemo.serialnumber in self._added_serial_numbers: return - component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN) + component = WEMO_MODEL_DISPATCH.get(wemo.model_name, SWITCH_DOMAIN) + device = await async_register_device(hass, self._config_entry, wemo) # Three cases: # - First time we see component, we need to load it and initialize the backlog @@ -171,7 +178,7 @@ class WemoDispatcher: device, ) - self._added_serial_numbers.add(device.serialnumber) + self._added_serial_numbers.add(wemo.serialnumber) class WemoDiscovery: @@ -200,7 +207,7 @@ class WemoDiscovery: for device in await self._hass.async_add_executor_job( pywemo.discover_devices ): - self._wemo_dispatcher.async_add_unique_device(self._hass, device) + await self._wemo_dispatcher.async_add_unique_device(self._hass, device) await self.discover_statics() finally: @@ -236,7 +243,9 @@ class WemoDiscovery: ], ): if device: - self._wemo_dispatcher.async_add_unique_device(self._hass, device) + await self._wemo_dispatcher.async_add_unique_device( + self._hass, device + ) def validate_static_config(host, port): diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py index e9272d39bdd..79972affa48 100644 --- a/homeassistant/components/wemo/const.py +++ b/homeassistant/components/wemo/const.py @@ -3,3 +3,6 @@ DOMAIN = "wemo" SERVICE_SET_HUMIDITY = "set_humidity" SERVICE_RESET_FILTER_LIFE = "reset_filter_life" +SIGNAL_WEMO_STATE_PUSH = f"{DOMAIN}.state_push" + +WEMO_SUBSCRIPTION_EVENT = f"{DOMAIN}_subscription_event" diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py new file mode 100644 index 00000000000..ba2ac08ed74 --- /dev/null +++ b/homeassistant/components/wemo/device_trigger.py @@ -0,0 +1,61 @@ +"""Triggers for WeMo devices.""" +from pywemo.subscribe import EVENT_TYPE_LONG_PRESS +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE + +from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT +from .wemo_device import async_get_device + +TRIGGER_TYPES = {EVENT_TYPE_LONG_PRESS} + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass, device_id): + """Return a list of triggers.""" + + wemo_trigger = { + # Required fields of TRIGGER_BASE_SCHEMA + CONF_PLATFORM: "device", + CONF_DOMAIN: WEMO_DOMAIN, + CONF_DEVICE_ID: device_id, + } + + device = async_get_device(hass, device_id) + triggers = [] + + # Check for long press support. + if device.supports_long_press: + triggers.append( + { + # Required fields of TRIGGER_SCHEMA + CONF_TYPE: EVENT_TYPE_LONG_PRESS, + **wemo_trigger, + } + ) + + return triggers + + +async def async_attach_trigger(hass, config, action, automation_info): + """Attach a trigger.""" + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: "event", + event_trigger.CONF_EVENT_TYPE: WEMO_SUBSCRIPTION_EVENT, + event_trigger.CONF_EVENT_DATA: { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + CONF_TYPE: config[CONF_TYPE], + }, + } + ) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 810ad74b953..19035367ae5 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -10,9 +10,11 @@ import async_timeout from pywemo import WeMoDevice from pywemo.exceptions import ActionException +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import DOMAIN as WEMO_DOMAIN +from .const import DOMAIN as WEMO_DOMAIN, SIGNAL_WEMO_STATE_PUSH +from .wemo_device import DeviceWrapper _LOGGER = logging.getLogger(__name__) @@ -35,9 +37,9 @@ class WemoEntity(Entity): Requires that subclasses implement the _update method. """ - def __init__(self, device: WeMoDevice) -> None: + def __init__(self, wemo: WeMoDevice) -> None: """Initialize the WeMo device.""" - self.wemo = device + self.wemo = wemo self._state = None self._available = True self._update_lock = None @@ -120,6 +122,12 @@ class WemoEntity(Entity): class WemoSubscriptionEntity(WemoEntity): """Common methods for Wemo devices that register for update callbacks.""" + def __init__(self, device: DeviceWrapper) -> None: + """Initialize WemoSubscriptionEntity.""" + super().__init__(device.wemo) + self._device_id = device.device_id + self._device_info = device.device_info + @property def unique_id(self) -> str: """Return the id of this WeMo device.""" @@ -128,12 +136,7 @@ class WemoSubscriptionEntity(WemoEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "name": self.name, - "identifiers": {(WEMO_DOMAIN, self.unique_id)}, - "model": self.wemo.model_name, - "manufacturer": "Belkin", - } + return self._device_info @property def is_on(self) -> bool: @@ -169,27 +172,25 @@ class WemoSubscriptionEntity(WemoEntity): """Wemo device added to Home Assistant.""" await super().async_added_to_hass() - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_WEMO_STATE_PUSH, self._async_subscription_callback + ) + ) - async def async_will_remove_from_hass(self) -> None: - """Wemo device removed from hass.""" - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.unregister, self.wemo) - - def _subscription_callback( - self, _device: WeMoDevice, _type: str, _params: str + async def _async_subscription_callback( + self, device_id: str, event_type: str, params: str ) -> None: """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update: bool) -> None: - """Handle an update from a subscription.""" + # Only respond events for this device. + if device_id != self._device_id: + return # If an update is in progress, we don't do anything if self._update_lock.locked(): return - await self._async_locked_update(force_update) + _LOGGER.debug("Subscription event (%s) for %s", event_type, self.name) + updated = await self.hass.async_add_executor_job( + self.wemo.subscription_update, event_type, params + ) + await self._async_locked_update(not updated) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index bbcdafaf351..79f2e9b7172 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -40,11 +40,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _discovered_wemo(device): """Handle a discovered Wemo device.""" - if device.model_name == "Dimmer": + if device.wemo.model_name == "Dimmer": async_add_entities([WemoDimmer(device)]) else: await hass.async_add_executor_job( - setup_bridge, hass, device, async_add_entities + setup_bridge, hass, device.wemo, async_add_entities ) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json index f7c6329b1af..3419b2cb3d1 100644 --- a/homeassistant/components/wemo/strings.json +++ b/homeassistant/components/wemo/strings.json @@ -9,5 +9,10 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo button was pressed for 2 seconds" + } } } diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py new file mode 100644 index 00000000000..3b0fbdcbe55 --- /dev/null +++ b/homeassistant/components/wemo/wemo_device.py @@ -0,0 +1,96 @@ +"""Home Assistant wrapper for a pyWeMo device.""" +import logging + +from pywemo import PyWeMoException, WeMoDevice +from pywemo.subscribe import EVENT_TYPE_LONG_PRESS + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_NAME, + CONF_PARAMS, + CONF_TYPE, + CONF_UNIQUE_ID, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import DOMAIN, SIGNAL_WEMO_STATE_PUSH, WEMO_SUBSCRIPTION_EVENT + +_LOGGER = logging.getLogger(__name__) + + +class DeviceWrapper: + """Home Assistant wrapper for a pyWeMo device.""" + + def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None: + """Initialize DeviceWrapper.""" + self.hass = hass + self.wemo = wemo + self.device_id = device_id + self.device_info = _device_info(wemo) + self.supports_long_press = wemo.supports_long_press() + + def subscription_callback( + self, _device: WeMoDevice, event_type: str, params: str + ) -> None: + """Receives push notifications from WeMo devices.""" + if event_type == EVENT_TYPE_LONG_PRESS: + self.hass.bus.fire( + WEMO_SUBSCRIPTION_EVENT, + { + CONF_DEVICE_ID: self.device_id, + CONF_NAME: self.wemo.name, + CONF_TYPE: event_type, + CONF_PARAMS: params, + CONF_UNIQUE_ID: self.wemo.serialnumber, + }, + ) + else: + dispatcher_send( + self.hass, SIGNAL_WEMO_STATE_PUSH, self.device_id, event_type, params + ) + + +def _device_info(wemo: WeMoDevice): + return { + "name": wemo.name, + "identifiers": {(DOMAIN, wemo.serialnumber)}, + "model": wemo.model_name, + "manufacturer": "Belkin", + } + + +async def async_register_device( + hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice +) -> DeviceWrapper: + """Register a device with home assistant and enable pywemo event callbacks.""" + device_registry = async_get_device_registry(hass) + entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, **_device_info(wemo) + ) + + registry = hass.data[DOMAIN]["registry"] + await hass.async_add_executor_job(registry.register, wemo) + + device = DeviceWrapper(hass, wemo, entry.id) + hass.data[DOMAIN].setdefault("devices", {})[entry.id] = device + registry.on(wemo, None, device.subscription_callback) + + if device.supports_long_press: + try: + await hass.async_add_executor_job(wemo.ensure_long_press_virtual_device) + except PyWeMoException: + _LOGGER.warning( + "Failed to enable long press support for device: %s", wemo.name + ) + device.supports_long_press = False + + return device + + +@callback +def async_get_device(hass: HomeAssistant, device_id: str) -> DeviceWrapper: + """Return DeviceWrapper for device_id.""" + return hass.data[DOMAIN]["devices"][device_id] diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 69b4b84dcd3..ba1995e8c83 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -43,13 +43,15 @@ def pywemo_registry_fixture(): @pytest.fixture(name="pywemo_device") def pywemo_device_fixture(pywemo_registry, pywemo_model): """Fixture for WeMoDevice instances.""" - device = create_autospec(getattr(pywemo, pywemo_model), instance=True) + cls = getattr(pywemo, pywemo_model) + device = create_autospec(cls, instance=True) device.host = MOCK_HOST device.port = MOCK_PORT device.name = MOCK_NAME device.serialnumber = MOCK_SERIAL_NUMBER device.model_name = pywemo_model device.get_state.return_value = 0 # Default to Off + device.supports_long_press.return_value = cls.supports_long_press() url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" with patch("pywemo.setup_url_for_address", return_value=url), patch( diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index e584cb5fb39..9289d4a0171 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -13,19 +13,31 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) +from homeassistant.components.wemo.const import SIGNAL_WEMO_STATE_PUSH from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component def _perform_registry_callback(hass, pywemo_registry, pywemo_device): """Return a callable method to trigger a state callback from the device.""" - @callback - def async_callback(): + async def async_callback(): + event = asyncio.Event() + + async def event_callback(e, *args): + event.set() + + stop_dispatcher_listener = async_dispatcher_connect( + hass, SIGNAL_WEMO_STATE_PUSH, event_callback + ) # Cause a state update callback to be triggered by the device. - pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") - return hass.async_block_till_done() + await hass.async_add_executor_job( + pywemo_registry.callbacks[pywemo_device.name], pywemo_device, "", "" + ) + await event.wait() + stop_dispatcher_listener() return async_callback @@ -63,8 +75,10 @@ async def _async_multiple_call_helper( """ # get_state is called outside the event loop. Use non-async Python Event. event = threading.Event() + waiting = asyncio.Event() def get_update(force_update=True): + hass.add_job(waiting.set) event.wait() update_polling_method = update_polling_method or pywemo_device.get_state @@ -77,6 +91,7 @@ async def _async_multiple_call_helper( ) # Allow the blocked call to return. + await waiting.wait() event.set() if pending: await asyncio.wait(pending) diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py new file mode 100644 index 00000000000..76016469b72 --- /dev/null +++ b/tests/components/wemo/test_device_trigger.py @@ -0,0 +1,98 @@ +"""Verify that WeMo device triggers work as expected.""" +import pytest +from pywemo.subscribe import EVENT_TYPE_LONG_PRESS + +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.wemo.const import DOMAIN, WEMO_SUBSCRIPTION_EVENT +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + +MOCK_DEVICE_ID = "some-device-id" +DATA_MESSAGE = {"message": "service-called"} + + +@pytest.fixture +def pywemo_model(): + """Pywemo Dimmer models use the light platform (WemoDimmer class).""" + return "Dimmer" + + +async def setup_automation(hass, device_id, trigger_type): + """Set up an automation trigger for testing triggering.""" + return await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: trigger_type, + }, + "action": { + "service": "test.automation", + "data": DATA_MESSAGE, + }, + }, + ] + }, + ) + + +async def test_get_triggers(hass, wemo_entity): + """Test that the triggers appear for a supported device.""" + assert wemo_entity.device_id is not None + + expected_triggers = [ + { + CONF_DEVICE_ID: wemo_entity.device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: EVENT_TYPE_LONG_PRESS, + }, + { + CONF_DEVICE_ID: wemo_entity.device_id, + CONF_DOMAIN: LIGHT_DOMAIN, + CONF_ENTITY_ID: wemo_entity.entity_id, + CONF_PLATFORM: "device", + CONF_TYPE: "turned_off", + }, + { + CONF_DEVICE_ID: wemo_entity.device_id, + CONF_DOMAIN: LIGHT_DOMAIN, + CONF_ENTITY_ID: wemo_entity.entity_id, + CONF_PLATFORM: "device", + CONF_TYPE: "turned_on", + }, + ] + triggers = await async_get_device_automations( + hass, "trigger", wemo_entity.device_id + ) + assert_lists_same(triggers, expected_triggers) + + +async def test_fires_on_long_press(hass): + """Test wemo long press trigger firing.""" + assert await setup_automation(hass, MOCK_DEVICE_ID, EVENT_TYPE_LONG_PRESS) + calls = async_mock_service(hass, "test", "automation") + + message = {CONF_DEVICE_ID: MOCK_DEVICE_ID, CONF_TYPE: EVENT_TYPE_LONG_PRESS} + hass.bus.async_fire(WEMO_SUBSCRIPTION_EVENT, message) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data == DATA_MESSAGE diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index c44bdb659c5..f34e9bd0471 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -110,6 +110,7 @@ async def test_discovery(hass, pywemo_registry): device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}" device.model_name = "Motion" device.get_state.return_value = 0 # Default to Off + device.supports_long_press.return_value = False return device pywemo_devices = [create_device(0), create_device(1)] diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py new file mode 100644 index 00000000000..38727a28424 --- /dev/null +++ b/tests/components/wemo/test_wemo_device.py @@ -0,0 +1,40 @@ +"""Tests for wemo_device.py.""" +from unittest.mock import patch + +import pytest +from pywemo import PyWeMoException + +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, wemo_device +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from .conftest import MOCK_HOST + + +@pytest.fixture +def pywemo_model(): + """Pywemo Dimmer models use the light platform (WemoDimmer class).""" + return "Dimmer" + + +async def test_async_register_device_longpress_fails(hass, pywemo_device): + """Device is still registered if ensure_long_press_virtual_device fails.""" + with patch.object(pywemo_device, "ensure_long_press_virtual_device") as elp: + elp.side_effect = PyWeMoException + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [MOCK_HOST], + }, + }, + ) + await hass.async_block_till_done() + dr = device_registry.async_get(hass) + device_entries = list(dr.devices.values()) + assert len(device_entries) == 1 + device_wrapper = wemo_device.async_get_device(hass, device_entries[0].id) + assert device_wrapper.supports_long_press is False From 7ad91fdf71ed8c1a2ded7ccc78ba1c41f9f4c7dd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 16 Jun 2021 12:11:23 +0200 Subject: [PATCH 387/750] Add swap to climate and change data_count -> count in modbus (#51668) --- homeassistant/components/modbus/__init__.py | 50 ++++++++++++--------- homeassistant/components/modbus/climate.py | 31 ++++++++++--- tests/components/modbus/test_climate.py | 11 ++--- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 382fe63ab69..8c37ea04079 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -187,28 +187,34 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ) -CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( - { - vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, - ] - ), - vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( - [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] - ), - vol.Optional(CONF_PRECISION, default=1): cv.positive_int, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, - vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, - vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), - vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string, - vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, - } +CLIMATE_SCHEMA = vol.All( + cv.deprecated(CONF_DATA_COUNT, replacement_key=CONF_COUNT), + BASE_COMPONENT_SCHEMA.extend( + { + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [ + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + ] + ), + vol.Required(CONF_TARGET_TEMP): cv.positive_int, + vol.Optional(CONF_COUNT, default=2): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( + [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] + ), + vol.Optional(CONF_PRECISION, default=1): cv.positive_int, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, + vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, + vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( + [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] + ), + } + ), ) COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 15a14b7eca9..a178f6f0f84 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + CONF_COUNT, CONF_NAME, CONF_OFFSET, CONF_STRUCTURE, @@ -28,13 +29,16 @@ from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, - CONF_DATA_COUNT, CONF_DATA_TYPE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_PRECISION, CONF_SCALE, CONF_STEP, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, DATA_TYPE_CUSTOM, DEFAULT_STRUCT_FORMAT, @@ -59,7 +63,7 @@ async def async_setup_platform( entities = [] for entity in discovery_info[CONF_CLIMATES]: hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] - count = entity[CONF_DATA_COUNT] + count = entity[CONF_COUNT] data_type = entity[CONF_DATA_TYPE] name = entity[CONF_NAME] structure = entity[CONF_STRUCTURE] @@ -110,7 +114,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._current_temperature = None self._data_type = config[CONF_DATA_TYPE] self._structure = config[CONF_STRUCTURE] - self._count = config[CONF_DATA_COUNT] + self._count = config[CONF_COUNT] self._precision = config[CONF_PRECISION] self._scale = config[CONF_SCALE] self._offset = config[CONF_OFFSET] @@ -118,6 +122,7 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] self._temp_step = config[CONF_STEP] + self._swap = config[CONF_SWAP] async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -194,6 +199,21 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._available = result is not None await self.async_update() + def _swap_registers(self, registers): + """Do swap as needed.""" + if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: + # convert [12][34] --> [21][43] + for i, register in enumerate(registers): + registers[i] = int.from_bytes( + register.to_bytes(2, byteorder="little"), + byteorder="big", + signed=False, + ) + if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: + # convert [12][34] ==> [34][12] + registers.reverse() + return registers + async def async_update(self, now=None): """Update Target & Current Temperature.""" # remark "now" is a dummy parameter to avoid problems with @@ -216,9 +236,8 @@ class ModbusThermostat(BasePlatform, RestoreEntity, ClimateEntity): self._available = False return -1 - byte_string = b"".join( - [x.to_bytes(2, byteorder="big") for x in result.registers] - ) + registers = self._swap_registers(result.registers) + byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) val = struct.unpack(self._structure, byte_string) if len(val) != 1 or not isinstance(val[0], (float, int)): _LOGGER.error( diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 50ba518bb36..c5201593480 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -3,14 +3,11 @@ import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import HVAC_MODE_AUTO -from homeassistant.components.modbus.const import ( - CONF_CLIMATES, - CONF_DATA_COUNT, - CONF_TARGET_TEMP, -) +from homeassistant.components.modbus.const import CONF_CLIMATES, CONF_TARGET_TEMP from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, + CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, @@ -28,7 +25,7 @@ from tests.common import mock_restore_cache {}, { CONF_SCAN_INTERVAL: 20, - CONF_DATA_COUNT: 2, + CONF_COUNT: 2, }, ], ) @@ -73,7 +70,7 @@ async def test_temperature_climate(hass, regs, expected): CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, - CONF_DATA_COUNT: 2, + CONF_COUNT: 2, }, climate_name, CLIMATE_DOMAIN, From 4655e3aa08f0d487250fcad6204a04022374b6cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 14:30:25 +0200 Subject: [PATCH 388/750] Clean up light group (#51922) --- homeassistant/components/group/light.py | 212 ++++++------------------ 1 file changed, 55 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 0abe842af2c..3f5a6eaf13e 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import Counter from collections.abc import Iterator import itertools -from typing import Any, Callable, cast +from typing import Any, Callable, Set, cast import voluptuous as vol @@ -42,16 +42,14 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.core import CoreState, Event, HomeAssistant, State import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType from . import GroupEntity -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - DEFAULT_NAME = "Light Group" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -67,7 +65,10 @@ SUPPORT_GROUP_LIGHT = ( async def async_setup_platform( - hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, ) -> None: """Initialize light.group platform.""" async_add_entities( @@ -78,33 +79,25 @@ async def async_setup_platform( class LightGroup(GroupEntity, light.LightEntity): """Representation of a light group.""" + _attr_available = False + _attr_icon = "mdi:lightbulb-group" + _attr_is_on = False + _attr_max_mireds = 500 + _attr_min_mireds = 154 + _attr_should_poll = False + def __init__(self, name: str, entity_ids: list[str]) -> None: """Initialize a light group.""" - self._name = name self._entity_ids = entity_ids - self._is_on = False - self._available = False - self._icon = "mdi:lightbulb-group" - self._brightness: int | None = None - self._color_mode: str | None = None - self._hs_color: tuple[float, float] | None = None - self._rgb_color: tuple[int, int, int] | None = None - self._rgbw_color: tuple[int, int, int, int] | None = None - self._rgbww_color: tuple[int, int, int, int, int] | None = None - self._xy_color: tuple[float, float] | None = None - self._color_temp: int | None = None - self._min_mireds: int = 154 - self._max_mireds: int = 500 self._white_value: int | None = None - self._effect_list: list[str] | None = None - self._effect: str | None = None - self._supported_color_modes: set[str] | None = None - self._supported_features: int = 0 + + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} async def async_added_to_hass(self) -> None: """Register callbacks.""" - async def async_state_changed_listener(event): + async def async_state_changed_listener(event: Event) -> None: """Handle child updates.""" self.async_set_context(event.context) await self.async_defer_or_update_ha_state() @@ -121,112 +114,12 @@ class LightGroup(GroupEntity, light.LightEntity): await super().async_added_to_hass() - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def is_on(self) -> bool: - """Return the on/off state of the light group.""" - return self._is_on - - @property - def available(self) -> bool: - """Return whether the light group is available.""" - return self._available - - @property - def icon(self): - """Return the light group icon.""" - return self._icon - - @property - def brightness(self) -> int | None: - """Return the brightness of this light group between 0..255.""" - return self._brightness - - @property - def color_mode(self) -> str | None: - """Return the color mode of the light.""" - return self._color_mode - - @property - def hs_color(self) -> tuple[float, float] | None: - """Return the HS color value [float, float].""" - return self._hs_color - - @property - def rgb_color(self) -> tuple[int, int, int] | None: - """Return the rgb color value [int, int, int].""" - return self._rgb_color - - @property - def rgbw_color(self) -> tuple[int, int, int, int] | None: - """Return the rgbw color value [int, int, int, int].""" - return self._rgbw_color - - @property - def rgbww_color(self) -> tuple[int, int, int, int, int] | None: - """Return the rgbww color value [int, int, int, int, int].""" - return self._rgbww_color - - @property - def xy_color(self) -> tuple[float, float] | None: - """Return the xy color value [float, float].""" - return self._xy_color - - @property - def color_temp(self) -> int | None: - """Return the CT color value in mireds.""" - return self._color_temp - - @property - def min_mireds(self) -> int: - """Return the coldest color_temp that this light group supports.""" - return self._min_mireds - - @property - def max_mireds(self) -> int: - """Return the warmest color_temp that this light group supports.""" - return self._max_mireds - @property def white_value(self) -> int | None: """Return the white value of this light group between 0..255.""" return self._white_value - @property - def effect_list(self) -> list[str] | None: - """Return the list of supported effects.""" - return self._effect_list - - @property - def effect(self) -> str | None: - """Return the current effect.""" - return self._effect - - @property - def supported_color_modes(self) -> set | None: - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - - @property - def should_poll(self) -> bool: - """No polling needed for a light group.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes for the light group.""" - return {ATTR_ENTITY_ID: self._entity_ids} - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} @@ -271,7 +164,7 @@ class LightGroup(GroupEntity, light.LightEntity): context=self._context, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Forward the turn_off command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} @@ -286,57 +179,60 @@ class LightGroup(GroupEntity, light.LightEntity): context=self._context, ) - async def async_update(self): + async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._is_on = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE for state in states) + self._attr_is_on = len(on_states) > 0 + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) + self._attr_brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - - self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple) - self._rgb_color = _reduce_attribute( + self._attr_hs_color = _reduce_attribute( + on_states, ATTR_HS_COLOR, reduce=_mean_tuple + ) + self._attr_rgb_color = _reduce_attribute( on_states, ATTR_RGB_COLOR, reduce=_mean_tuple ) - self._rgbw_color = _reduce_attribute( + self._attr_rgbw_color = _reduce_attribute( on_states, ATTR_RGBW_COLOR, reduce=_mean_tuple ) - self._rgbww_color = _reduce_attribute( + self._attr_rgbww_color = _reduce_attribute( on_states, ATTR_RGBWW_COLOR, reduce=_mean_tuple ) - self._xy_color = _reduce_attribute(on_states, ATTR_XY_COLOR, reduce=_mean_tuple) + self._attr_xy_color = _reduce_attribute( + on_states, ATTR_XY_COLOR, reduce=_mean_tuple + ) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) - self._color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP) - self._min_mireds = _reduce_attribute( + self._attr_color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP) + self._attr_min_mireds = _reduce_attribute( states, ATTR_MIN_MIREDS, default=154, reduce=min ) - self._max_mireds = _reduce_attribute( + self._attr_max_mireds = _reduce_attribute( states, ATTR_MAX_MIREDS, default=500, reduce=max ) - self._effect_list = None + self._attr_effect_list = None all_effect_lists = list(_find_state_attributes(states, ATTR_EFFECT_LIST)) if all_effect_lists: # Merge all effects from all effect_lists with a union merge. - self._effect_list = list(set().union(*all_effect_lists)) - self._effect_list.sort() - if "None" in self._effect_list: - self._effect_list.remove("None") - self._effect_list.insert(0, "None") + self._attr_effect_list = list(set().union(*all_effect_lists)) + self._attr_effect_list.sort() + if "None" in self._attr_effect_list: + self._attr_effect_list.remove("None") + self._attr_effect_list.insert(0, "None") - self._effect = None + self._attr_effect = None all_effects = list(_find_state_attributes(on_states, ATTR_EFFECT)) if all_effects: # Report the most common effect. effects_count = Counter(itertools.chain(all_effects)) - self._effect = effects_count.most_common(1)[0][0] + self._attr_effect = effects_count.most_common(1)[0][0] - self._color_mode = None + self._attr_color_mode = None all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE)) if all_color_modes: # Report the most common color mode, select brightness and onoff last @@ -345,24 +241,26 @@ class LightGroup(GroupEntity, light.LightEntity): color_mode_count[COLOR_MODE_ONOFF] = -1 if COLOR_MODE_BRIGHTNESS in color_mode_count: color_mode_count[COLOR_MODE_BRIGHTNESS] = 0 - self._color_mode = color_mode_count.most_common(1)[0][0] + self._attr_color_mode = color_mode_count.most_common(1)[0][0] - self._supported_color_modes = None + self._attr_supported_color_modes = None all_supported_color_modes = list( _find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. - self._supported_color_modes = set().union(*all_supported_color_modes) + self._attr_supported_color_modes = cast( + Set[str], set().union(*all_supported_color_modes) + ) - self._supported_features = 0 + self._attr_supported_features = 0 for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. - self._supported_features |= support + self._attr_supported_features |= support # Bitwise-and the supported features with the GroupedLight's features # so that we don't break in the future when a new feature is added. - self._supported_features &= SUPPORT_GROUP_LIGHT + self._attr_supported_features &= SUPPORT_GROUP_LIGHT def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]: @@ -373,12 +271,12 @@ def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]: yield value -def _mean_int(*args): +def _mean_int(*args: Any) -> int: """Return the mean of the supplied values.""" return int(sum(args) / len(args)) -def _mean_tuple(*args): +def _mean_tuple(*args: Any) -> tuple[float | Any, ...]: """Return the mean values along the columns of the supplied values.""" return tuple(sum(x) / len(x) for x in zip(*args)) From 13bf5dbee4c6472521438cce9d2b0d27394dc5a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 14:35:32 +0200 Subject: [PATCH 389/750] Upgrade mypy to 0.902 (#51907) --- homeassistant/util/yaml/loader.py | 6 +++--- requirements_test.txt | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 5e98e4cfc6f..58edac6d280 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -98,10 +98,10 @@ class SafeLineLoader(yaml.SafeLoader): super().__init__(stream) self.secrets = secrets - def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: + def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: # type: ignore[override] """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super().compose_node(parent, index) + node: yaml.nodes.Node = super().compose_node(parent, index) # type: ignore[assignment] node.__line__ = last_line + 1 # type: ignore return node @@ -264,7 +264,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order fname = getattr(loader.stream, "name", "") raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', - context_mark=yaml.Mark(fname, 0, line, -1, None, None), + context_mark=yaml.Mark(fname, 0, line, -1, None, None), # type: ignore[arg-type] ) from exc if key in seen: diff --git a/requirements_test.txt b/requirements_test.txt index 25a542d3ffb..660ea2a11fd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ codecov==2.1.11 coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 -mypy==0.812 +mypy==0.902 pre-commit==2.13.0 pylint==2.8.3 pipdeptree==1.0.0 @@ -25,3 +25,19 @@ responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 tqdm==4.49.0 +types-backports==0.1.2 +types-certifi==0.1.3 +types-chardet==0.1.2 +types-cryptography==3.3.2 +types-decorator==0.1.4 +types-emoji==1.2.1 +types-enum34==0.1.5 +types-ipaddress==0.1.2 +types-jwt==0.1.3 +types-pkg-resources==0.1.2 +types-python-slugify==0.1.0 +types-pytz==0.1.1 +types-PyYAML==5.4.1 +types-requests==0.1.11 +types-toml==0.1.2 +types-ujson==0.1.0 From dc9f1411d46bf6ec0632cdb9fed78df9b5700eda Mon Sep 17 00:00:00 2001 From: djtimca <60706061+djtimca@users.noreply.github.com> Date: Wed, 16 Jun 2021 08:39:09 -0400 Subject: [PATCH 390/750] Add Omnilogic switch defaults for max_speed and min_speed (#51889) --- homeassistant/components/omnilogic/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index 771b02a24c1..8fffa384916 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -153,8 +153,8 @@ class OmniLogicPumpControl(OmniLogicSwitch): state_key=state_key, ) - self._max_speed = int(coordinator.data[item_id]["Max-Pump-Speed"]) - self._min_speed = int(coordinator.data[item_id]["Min-Pump-Speed"]) + self._max_speed = int(coordinator.data[item_id].get("Max-Pump-Speed", 100)) + self._min_speed = int(coordinator.data[item_id].get("Min-Pump-Speed", 0)) if "Filter-Type" in coordinator.data[item_id]: self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Filter-Type"]] From bc3e5b39ed402824b07a29766bf11df9bb8284a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 14:47:29 +0200 Subject: [PATCH 391/750] Clean up cover group (#51924) --- homeassistant/components/group/cover.py | 149 +++++++++--------------- 1 file changed, 56 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 5e8d18b28e2..b5022582d9e 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,6 +1,8 @@ """This platform allows several cover to be grouped into one cover.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.cover import ( @@ -38,15 +40,14 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import CoreState, State +from homeassistant.core import CoreState, Event, HomeAssistant, State import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType from . import GroupEntity -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - KEY_OPEN_CLOSE = "open_close" KEY_STOP = "stop" KEY_POSITION = "position" @@ -62,7 +63,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the Group Cover platform.""" async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) @@ -70,17 +76,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" - def __init__(self, name, entities): - """Initialize a CoverGroup entity.""" - self._name = name - self._is_closed = False - self._is_closing = False - self._is_opening = False - self._cover_position: int | None = 100 - self._tilt_position = None - self._supported_features = 0 - self._assumed_state = True + _attr_is_closed: bool | None = False + _attr_is_opening: bool | None = False + _attr_is_closing: bool | None = False + _attr_current_cover_position: int | None = 100 + _attr_assumed_state: bool = True + def __init__(self, name: str, entities: list[str]) -> None: + """Initialize a CoverGroup entity.""" self._entities = entities self._covers: dict[str, set[str]] = { KEY_OPEN_CLOSE: set(), @@ -93,11 +96,16 @@ class CoverGroup(GroupEntity, CoverEntity): KEY_POSITION: set(), } - async def _update_supported_features_event(self, event): + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} + + async def _update_supported_features_event(self, event: Event) -> None: self.async_set_context(event.context) - await self.async_update_supported_features( - event.data.get("entity_id"), event.data.get("new_state") - ) + entity = event.data.get("entity_id") + if entity is not None: + await self.async_update_supported_features( + entity, event.data.get("new_state") + ) async def async_update_supported_features( self, @@ -146,7 +154,7 @@ class CoverGroup(GroupEntity, CoverEntity): if update_state: await self.async_defer_or_update_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) @@ -166,73 +174,28 @@ class CoverGroup(GroupEntity, CoverEntity): return await super().async_added_to_hass() - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def assumed_state(self): - """Enable buttons even if at end position.""" - return self._assumed_state - - @property - def supported_features(self): - """Flag supported features for the cover.""" - return self._supported_features - - @property - def is_closed(self): - """Return if all covers in group are closed.""" - return self._is_closed - - @property - def is_opening(self): - """Return if the cover is opening or not.""" - return self._is_opening - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - return self._is_closing - - @property - def current_cover_position(self) -> int | None: - """Return current position for all covers.""" - return self._cover_position - - @property - def current_cover_tilt_position(self): - """Return current tilt position for all covers.""" - return self._tilt_position - - @property - def extra_state_attributes(self): - """Return the state attributes for the cover group.""" - return {ATTR_ENTITY_ID: self._entities} - - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Move the covers up.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context ) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Move the covers down.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True, context=self._context ) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Fire the stop action.""" data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} await self.hass.services.async_call( DOMAIN, SERVICE_STOP_COVER, data, blocking=True, context=self._context ) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set covers position.""" data = { ATTR_ENTITY_ID: self._covers[KEY_POSITION], @@ -246,28 +209,28 @@ class CoverGroup(GroupEntity, CoverEntity): context=self._context, ) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt covers open.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True, context=self._context ) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt covers closed.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} await self.hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True, context=self._context ) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} await self.hass.services.async_call( DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True, context=self._context ) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Set tilt position.""" data = { ATTR_ENTITY_ID: self._tilts[KEY_POSITION], @@ -281,31 +244,31 @@ class CoverGroup(GroupEntity, CoverEntity): context=self._context, ) - async def async_update(self): + async def async_update(self) -> None: """Update state and attributes.""" - self._assumed_state = False + self._attr_assumed_state = False - self._is_closed = True - self._is_closing = False - self._is_opening = False + self._attr_is_closed = True + self._attr_is_closing = False + self._attr_is_opening = False for entity_id in self._entities: state = self.hass.states.get(entity_id) if not state: continue if state.state == STATE_OPEN: - self._is_closed = False + self._attr_is_closed = False break if state.state == STATE_CLOSING: - self._is_closing = True + self._attr_is_closing = True break if state.state == STATE_OPENING: - self._is_opening = True + self._attr_is_opening = True break - self._cover_position = None + self._attr_current_cover_position = None if self._covers[KEY_POSITION]: - position = -1 - self._cover_position = 0 if self.is_closed else 100 + position: int | None = -1 + self._attr_current_cover_position = 0 if self.is_closed else 100 for entity_id in self._covers[KEY_POSITION]: state = self.hass.states.get(entity_id) if state is None: @@ -314,16 +277,16 @@ class CoverGroup(GroupEntity, CoverEntity): if position == -1: position = pos elif position != pos: - self._assumed_state = True + self._attr_assumed_state = True break else: if position != -1: - self._cover_position = position + self._attr_current_cover_position = position - self._tilt_position = None + self._attr_current_cover_tilt_position = None if self._tilts[KEY_POSITION]: position = -1 - self._tilt_position = 100 + self._attr_current_cover_tilt_position = 100 for entity_id in self._tilts[KEY_POSITION]: state = self.hass.states.get(entity_id) if state is None: @@ -332,11 +295,11 @@ class CoverGroup(GroupEntity, CoverEntity): if position == -1: position = pos elif position != pos: - self._assumed_state = True + self._attr_assumed_state = True break else: if position != -1: - self._tilt_position = position + self._attr_current_cover_tilt_position = position supported_features = 0 supported_features |= ( @@ -351,13 +314,13 @@ class CoverGroup(GroupEntity, CoverEntity): supported_features |= ( SUPPORT_SET_TILT_POSITION if self._tilts[KEY_POSITION] else 0 ) - self._supported_features = supported_features + self._attr_supported_features = supported_features - if not self._assumed_state: + if not self._attr_assumed_state: for entity_id in self._entities: state = self.hass.states.get(entity_id) if state is None: continue if state and state.attributes.get(ATTR_ASSUMED_STATE): - self._assumed_state = True + self._attr_assumed_state = True break From 31db3fcb233e200029b5663af4aa4889bb1880ee Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 16 Jun 2021 10:30:05 -0500 Subject: [PATCH 392/750] Refactor Sonos alarms and favorites into system-level coordinators (#51757) * Refactor alarms and favorites into household-level coordinators Create SonosHouseholdCoodinator class for system-level data Fix polling for both alarms and favorites Adjust tests * Fix docstring * Review cleanup * Move exception handling up a level, do not save a failed coordinator * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/sonos/__init__.py | 36 ++++----- homeassistant/components/sonos/alarms.py | 70 ++++++++++++++++ homeassistant/components/sonos/const.py | 4 +- homeassistant/components/sonos/entity.py | 4 +- homeassistant/components/sonos/favorites.py | 81 ++++++------------- .../components/sonos/household_coordinator.py | 74 +++++++++++++++++ .../components/sonos/media_player.py | 4 + homeassistant/components/sonos/speaker.py | 74 ++++++----------- homeassistant/components/sonos/switch.py | 50 ++++++------ tests/components/sonos/test_switch.py | 19 +++-- 10 files changed, 255 insertions(+), 161 deletions(-) create mode 100644 homeassistant/components/sonos/alarms.py create mode 100644 homeassistant/components/sonos/household_coordinator.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index a20805ce136..218ddaa8e15 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections import OrderedDict, deque +from collections import OrderedDict import datetime from enum import Enum import logging @@ -11,7 +11,6 @@ from urllib.parse import urlparse import pysonos from pysonos import events_asyncio -from pysonos.alarms import Alarm from pysonos.core import SoCo from pysonos.exceptions import SoCoException import voluptuous as vol @@ -29,12 +28,12 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from .alarms import SonosAlarms from .const import ( DATA_SONOS, DISCOVERY_INTERVAL, DOMAIN, PLATFORMS, - SONOS_ALARM_UPDATE, SONOS_GROUP_UPDATE, SONOS_REBOOTED, SONOS_SEEN, @@ -82,11 +81,10 @@ class SonosData: def __init__(self) -> None: """Initialize the data.""" - # OrderedDict behavior used by SonosFavorites + # OrderedDict behavior used by SonosAlarms and SonosFavorites self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict() self.favorites: dict[str, SonosFavorites] = {} - self.alarms: dict[str, Alarm] = {} - self.processed_alarm_events = deque(maxlen=5) + self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() self.hosts_heartbeat = None self.ssdp_known: set[str] = set() @@ -148,14 +146,17 @@ async def async_setup_entry( # noqa: C901 _LOGGER.debug("Adding new speaker: %s", speaker_info) speaker = SonosSpeaker(hass, soco, speaker_info) data.discovered[soco.uid] = speaker - if soco.household_id not in data.favorites: - data.favorites[soco.household_id] = SonosFavorites( - hass, soco.household_id - ) - data.favorites[soco.household_id].update() + for coordinator, coord_dict in [ + (SonosAlarms, data.alarms), + (SonosFavorites, data.favorites), + ]: + if soco.household_id not in coord_dict: + new_coordinator = coordinator(hass, soco.household_id) + new_coordinator.setup(soco) + coord_dict[soco.household_id] = new_coordinator speaker.setup() - except SoCoException as ex: - _LOGGER.debug("SoCoException, ex=%s", ex) + except (OSError, SoCoException): + _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None: """Create a soco instance and return if successful.""" @@ -236,10 +237,6 @@ async def async_setup_entry( # noqa: C901 _async_create_discovered_player(uid, discovered_ip, boot_seqnum) ) - @callback - def _async_signal_update_alarms(event): - async_dispatcher_send(hass, SONOS_ALARM_UPDATE) - async def setup_platforms_and_discovery(): await asyncio.gather( *[ @@ -252,11 +249,6 @@ async def async_setup_entry( # noqa: C901 EVENT_HOMEASSISTANT_START, _async_signal_update_groups ) ) - entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, _async_signal_update_alarms - ) - ) entry.async_on_unload( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener diff --git a/homeassistant/components/sonos/alarms.py b/homeassistant/components/sonos/alarms.py new file mode 100644 index 00000000000..98e4b752cad --- /dev/null +++ b/homeassistant/components/sonos/alarms.py @@ -0,0 +1,70 @@ +"""Class representing Sonos alarms.""" +from __future__ import annotations + +from collections.abc import Iterator +import logging +from typing import Any + +from pysonos import SoCo +from pysonos.alarms import Alarm, get_alarms +from pysonos.exceptions import SoCoException + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM +from .household_coordinator import SonosHouseholdCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class SonosAlarms(SonosHouseholdCoordinator): + """Coordinator class for Sonos alarms.""" + + def __init__(self, *args: Any) -> None: + """Initialize the data.""" + super().__init__(*args) + self._alarms: dict[str, Alarm] = {} + + def __iter__(self) -> Iterator: + """Return an iterator for the known alarms.""" + alarms = list(self._alarms.values()) + return iter(alarms) + + def get(self, alarm_id: str) -> Alarm | None: + """Get an Alarm instance.""" + return self._alarms.get(alarm_id) + + async def async_update_entities(self, soco: SoCo) -> bool: + """Create and update alarms entities, return success.""" + try: + new_alarms = await self.hass.async_add_executor_job(self.update_cache, soco) + except (OSError, SoCoException) as err: + _LOGGER.error("Could not refresh alarms using %s: %s", soco, err) + return False + + for alarm in new_alarms: + speaker = self.hass.data[DATA_SONOS].discovered[alarm.zone.uid] + async_dispatcher_send( + self.hass, SONOS_CREATE_ALARM, speaker, [alarm.alarm_id] + ) + async_dispatcher_send(self.hass, f"{SONOS_ALARMS_UPDATED}-{self.household_id}") + return True + + def update_cache(self, soco: SoCo) -> set[Alarm]: + """Populate cache of known alarms. + + Prune deleted alarms and return new alarms. + """ + soco_alarms = get_alarms(soco) + new_alarms = set() + + for alarm in soco_alarms: + if alarm.alarm_id not in self._alarms: + new_alarms.add(alarm) + self._alarms[alarm.alarm_id] = alarm + + for alarm_id, alarm in list(self._alarms.items()): + if alarm not in soco_alarms: + self._alarms.pop(alarm_id) + + return new_alarms diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 84ccb99baed..9072f4cab02 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -140,8 +140,8 @@ SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_GROUP_UPDATE = "sonos_group_update" -SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" -SONOS_ALARM_UPDATE = "sonos_alarm_update" +SONOS_ALARMS_UPDATED = "sonos_alarms_updated" +SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_REBOOTED = "sonos_rebooted" SONOS_SEEN = "sonos_seen" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 290c6a64cb1..a2b0d7c5a64 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, SONOS_ENTITY_CREATED, - SONOS_HOUSEHOLD_UPDATED, + SONOS_FAVORITES_UPDATED, SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, ) @@ -54,7 +54,7 @@ class SonosEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SONOS_HOUSEHOLD_UPDATED}-{self.soco.household_id}", + f"{SONOS_FAVORITES_UPDATED}-{self.soco.household_id}", self.async_write_ha_state, ) ) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 2f5cab23be2..25fc58ebba2 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -2,85 +2,52 @@ from __future__ import annotations from collections.abc import Iterator -import datetime import logging -from typing import Callable +from typing import Any +from pysonos import SoCo from pysonos.data_structures import DidlFavorite -from pysonos.events_base import Event as SonosEvent from pysonos.exceptions import SoCoException -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import DATA_SONOS, SONOS_HOUSEHOLD_UPDATED +from .const import SONOS_FAVORITES_UPDATED +from .household_coordinator import SonosHouseholdCoordinator _LOGGER = logging.getLogger(__name__) -class SonosFavorites: - """Storage class for Sonos favorites.""" +class SonosFavorites(SonosHouseholdCoordinator): + """Coordinator class for Sonos favorites.""" - def __init__(self, hass: HomeAssistant, household_id: str) -> None: + def __init__(self, *args: Any) -> None: """Initialize the data.""" - self.hass = hass - self.household_id = household_id + super().__init__(*args) self._favorites: list[DidlFavorite] = [] - self._event_version: str | None = None - self._next_update: Callable | None = None def __iter__(self) -> Iterator: """Return an iterator for the known favorites.""" favorites = self._favorites.copy() return iter(favorites) - @callback - def async_delayed_update(self, event: SonosEvent) -> None: - """Add a delay when triggered by an event. + async def async_update_entities(self, soco: SoCo) -> bool: + """Update the cache and update entities.""" + try: + await self.hass.async_add_executor_job(self.update_cache, soco) + except (OSError, SoCoException) as err: + _LOGGER.warning("Error requesting favorites from %s: %s", soco, err) + return False - Updated favorites are not always immediately available. + async_dispatcher_send( + self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}" + ) + return True - """ - if not (event_id := event.variables.get("favorites_update_id")): - return - - if not self._event_version: - self._event_version = event_id - return - - if self._event_version == event_id: - _LOGGER.debug("Favorites haven't changed (event_id: %s)", event_id) - return - - self._event_version = event_id - - if self._next_update: - self._next_update() - - self._next_update = self.hass.helpers.event.async_call_later(3, self.update) - - def update(self, now: datetime.datetime | None = None) -> None: + def update_cache(self, soco: SoCo) -> None: """Request new Sonos favorites from a speaker.""" - new_favorites = None - discovered = self.hass.data[DATA_SONOS].discovered - - for uid, speaker in discovered.items(): - try: - new_favorites = speaker.soco.music_library.get_sonos_favorites() - except SoCoException as err: - _LOGGER.warning( - "Error requesting favorites from %s: %s", speaker.soco, err - ) - else: - # Prefer this SoCo instance next update - discovered.move_to_end(uid, last=False) - break - - if new_favorites is None: - _LOGGER.error("Could not reach any speakers to update favorites") - return - + new_favorites = soco.music_library.get_sonos_favorites() self._favorites = [] + for fav in new_favorites: try: # exclude non-playable favorites with no linked resources @@ -89,9 +56,9 @@ class SonosFavorites: except SoCoException as ex: # Skip unknown types _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) + _LOGGER.debug( "Cached %s favorites for household %s", len(self._favorites), self.household_id, ) - dispatcher_send(self.hass, f"{SONOS_HOUSEHOLD_UPDATED}-{self.household_id}") diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py new file mode 100644 index 00000000000..d24ab40b3db --- /dev/null +++ b/homeassistant/components/sonos/household_coordinator.py @@ -0,0 +1,74 @@ +"""Class representing a Sonos household storage helper.""" +from __future__ import annotations + +from collections import deque +from collections.abc import Callable, Coroutine +import logging +from typing import Any + +from pysonos import SoCo + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer + +from .const import DATA_SONOS + +_LOGGER = logging.getLogger(__name__) + + +class SonosHouseholdCoordinator: + """Base class for Sonos household-level storage.""" + + def __init__(self, hass: HomeAssistant, household_id: str) -> None: + """Initialize the data.""" + self.hass = hass + self.household_id = household_id + self._processed_events = deque(maxlen=5) + self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None + + def setup(self, soco: SoCo) -> None: + """Set up the SonosAlarm instance.""" + self.update_cache(soco) + self.hass.add_job(self._async_create_polling_debouncer) + + async def _async_create_polling_debouncer(self) -> None: + """Create a polling debouncer in async context. + + Used to ensure redundant poll requests from all speakers are coalesced. + """ + self.async_poll = Debouncer( + self.hass, + _LOGGER, + cooldown=3, + immediate=False, + function=self._async_poll, + ).async_call + + async def _async_poll(self) -> None: + """Poll any known speaker.""" + discovered = self.hass.data[DATA_SONOS].discovered + + for uid, speaker in discovered.items(): + _LOGGER.debug("Updating %s using %s", type(self).__name__, speaker.soco) + success = await self.async_update_entities(speaker.soco) + + if success: + # Prefer this SoCo instance next update + discovered.move_to_end(uid, last=False) + break + + @callback + def async_handle_event(self, event_id: str, soco: SoCo) -> None: + """Create a task to update from an event callback.""" + if event_id in self._processed_events: + return + self._processed_events.append(event_id) + self.hass.async_create_task(self.async_update_entities(soco)) + + async def async_update_entities(self, soco: SoCo) -> bool: + """Update the cache and update entities.""" + raise NotImplementedError() + + def update_cache(self, soco: SoCo) -> Any: + """Update the cache of the household-level feature.""" + raise NotImplementedError() diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c44f7fbd4fb..8b1664d4f6c 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -55,6 +55,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.network import is_internal_request from .const import ( + DATA_SONOS, DOMAIN as SONOS_DOMAIN, MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, @@ -295,6 +296,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_update(self) -> None: """Retrieve latest state by polling.""" + await self.hass.data[DATA_SONOS].favorites[ + self.speaker.household_id + ].async_poll() await self.hass.async_add_executor_job(self._update) def _update(self) -> None: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 59ed94adec6..88e6ac33ba7 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections import deque from collections.abc import Coroutine import contextlib import datetime @@ -12,7 +11,6 @@ from typing import Any, Callable import urllib.parse import async_timeout -from pysonos.alarms import get_alarms from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from pysonos.events_base import Event as SonosEvent, SubscriptionBase @@ -33,6 +31,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.util import dt as dt_util +from .alarms import SonosAlarms from .const import ( BATTERY_SCAN_INTERVAL, DATA_SONOS, @@ -40,7 +39,6 @@ from .const import ( PLATFORMS, SCAN_INTERVAL, SEEN_EXPIRE_TIME, - SONOS_ALARM_UPDATE, SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, @@ -225,7 +223,9 @@ class SonosSpeaker: else: self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) - if new_alarms := self.update_alarms_for_speaker(): + if new_alarms := [ + alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid + ]: dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) else: self._platforms_ready.add(SWITCH_DOMAIN) @@ -233,7 +233,7 @@ class SonosSpeaker: self._event_dispatchers = { "AlarmClock": self.async_dispatch_alarms, "AVTransport": self.async_dispatch_media_update, - "ContentDirectory": self.favorites.async_delayed_update, + "ContentDirectory": self.async_dispatch_favorites, "DeviceProperties": self.async_dispatch_device_properties, "RenderingControl": self.async_update_volume, "ZoneGroupTopology": self.async_update_groups, @@ -246,8 +246,11 @@ class SonosSpeaker: # async def async_handle_new_entity(self, entity_type: str) -> None: """Listen to new entities to trigger first subscription.""" + if self._platforms_ready == PLATFORMS: + return + self._platforms_ready.add(entity_type) - if self._platforms_ready == PLATFORMS and not self._subscriptions: + if self._platforms_ready == PLATFORMS: self._resubscription_lock = asyncio.Lock() await self.async_subscribe() self._is_ready = True @@ -274,6 +277,11 @@ class SonosSpeaker: """Return whether this speaker is available.""" return self._seen_timer is not None + @property + def alarms(self) -> SonosAlarms: + """Return the SonosAlarms instance for this household.""" + return self.hass.data[DATA_SONOS].alarms[self.household_id] + @property def favorites(self) -> SonosFavorites: """Return the SonosFavorites instance for this household.""" @@ -284,11 +292,6 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None - @property - def processed_alarm_events(self) -> deque[str]: - """Return the container of processed alarm events.""" - return self.hass.data[DATA_SONOS].processed_alarm_events - @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -381,13 +384,10 @@ class SonosSpeaker: @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: - """Create a task to update alarms from an event.""" - if not (update_id := event.variables.get("alarm_list_version")): + """Add the soco instance associated with the event to the callback.""" + if not (event_id := event.variables.get("alarm_list_version")): return - if update_id in self.processed_alarm_events: - return - self.processed_alarm_events.append(update_id) - self.hass.async_add_executor_job(self.update_alarms) + self.alarms.async_handle_event(event_id, self.soco) @callback def async_dispatch_device_properties(self, event: SonosEvent) -> None: @@ -401,6 +401,13 @@ class SonosSpeaker: await self.async_update_battery_info(battery_dict) self.async_write_entity_states() + @callback + def async_dispatch_favorites(self, event: SonosEvent) -> None: + """Add the soco instance associated with the event to the callback.""" + if not (event_id := event.variables.get("favorites_update_id")): + return + self.favorites.async_handle_event(event_id, self.soco) + @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: """Update information about currently playing media from an event.""" @@ -493,37 +500,6 @@ class SonosSpeaker: await self.async_unseen(will_reconnect=True) await self.async_seen(soco) - # - # Alarm management - # - def update_alarms_for_speaker(self) -> set[str]: - """Update current alarm instances. - - Updates hass.data[DATA_SONOS].alarms and returns a list of all alarms that are new. - """ - new_alarms = set() - stored_alarms = self.hass.data[DATA_SONOS].alarms - updated_alarms = get_alarms(self.soco) - - for alarm in updated_alarms: - if alarm.zone.uid == self.soco.uid and alarm.alarm_id not in list( - stored_alarms.keys() - ): - new_alarms.add(alarm.alarm_id) - stored_alarms[alarm.alarm_id] = alarm - - for alarm_id, alarm in list(stored_alarms.items()): - if alarm not in updated_alarms: - stored_alarms.pop(alarm_id) - - return new_alarms - - def update_alarms(self) -> None: - """Update alarms from an event.""" - if new_alarms := self.update_alarms_for_speaker(): - dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) - dispatcher_send(self.hass, SONOS_ALARM_UPDATE) - # # Battery management # @@ -618,7 +594,7 @@ class SonosSpeaker: coordinator_uid = self.soco.uid slave_uids = [] - with contextlib.suppress(SoCoException): + with contextlib.suppress(OSError, SoCoException): if self.soco.group and self.soco.group.coordinator: coordinator_uid = self.soco.group.coordinator.uid slave_uids = [ diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 4b24224f6a0..795eded6ec1 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime import logging -from pysonos.exceptions import SoCoUPnPException +from pysonos.exceptions import SoCoException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.const import ATTR_TIME @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, - SONOS_ALARM_UPDATE, + SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM, ) from .entity import SonosEntity @@ -35,15 +35,12 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - configured_alarms = set() - - async def _async_create_entity(speaker: SonosSpeaker, new_alarms: set) -> None: - for alarm_id in new_alarms: - if alarm_id not in configured_alarms: - _LOGGER.debug("Creating alarm with id %s", alarm_id) - entity = SonosAlarmEntity(alarm_id, speaker) - async_add_entities([entity]) - configured_alarms.add(alarm_id) + async def _async_create_entity(speaker: SonosSpeaker, alarm_ids: list[str]) -> None: + entities = [] + for alarm_id in alarm_ids: + _LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name) + entities.append(SonosAlarmEntity(alarm_id, speaker)) + async_add_entities(entities) config_entry.async_on_unload( async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) @@ -57,7 +54,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): """Initialize the switch.""" super().__init__(speaker) - self._alarm_id = alarm_id + self.alarm_id = alarm_id + self.household_id = speaker.household_id self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}") async def async_added_to_hass(self) -> None: @@ -66,20 +64,15 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - SONOS_ALARM_UPDATE, - self.async_update, + f"{SONOS_ALARMS_UPDATED}-{self.household_id}", + self.async_update_state, ) ) @property def alarm(self): """Return the alarm instance.""" - return self.hass.data[DATA_SONOS].alarms[self.alarm_id] - - @property - def alarm_id(self): - """Return the ID of the alarm.""" - return self._alarm_id + return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id) @property def unique_id(self) -> str: @@ -100,10 +93,14 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): str(self.alarm.start_time)[0:5], ) + async def async_update(self) -> None: + """Call the central alarm polling method.""" + await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() + @callback def async_check_if_available(self): """Check if alarm exists and remove alarm entity if not available.""" - if self.alarm_id in self.hass.data[DATA_SONOS].alarms: + if self.alarm: return True _LOGGER.debug("%s has been deleted", self.entity_id) @@ -114,7 +111,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return False - async def async_update(self) -> None: + async def async_update_state(self) -> None: """Poll the device for the current state.""" if not self.async_check_if_available(): return @@ -170,6 +167,11 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): or bool(recurrence == "WEEKENDS" and int(timestr) not in range(1, 7)) ) + @property + def available(self) -> bool: + """Return whether this alarm is available.""" + return (self.alarm is not None) and self.speaker.available + @property def is_on(self): """Return state of Sonos alarm switch.""" @@ -203,5 +205,5 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): _LOGGER.debug("Toggling the state of %s", self.entity_id) self.alarm.enabled = turn_on await self.hass.async_add_executor_job(self.alarm.save) - except SoCoUPnPException as exc: - _LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True) + except (OSError, SoCoException, SoCoUPnPException) as exc: + _LOGGER.error("Could not update %s: %s", self.entity_id, exc) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 41cb241d377..f684a8f351e 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -1,4 +1,6 @@ """Tests for the Sonos Alarm switch platform.""" +from copy import copy + from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.switch import ( ATTR_DURATION, @@ -52,24 +54,31 @@ async def test_alarm_create_delete( hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event ): """Test for correct creation and deletion of alarms during runtime.""" - soco.alarmClock = alarm_clock_extended + entity_registry = async_get_entity_registry(hass) + + one_alarm = copy(alarm_clock.ListAlarms.return_value) + two_alarms = copy(alarm_clock_extended.ListAlarms.return_value) await setup_platform(hass, config_entry, config) - subscription = alarm_clock_extended.subscribe.return_value + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" not in entity_registry.entities + + subscription = alarm_clock.subscribe.return_value sub_callback = subscription.callback + alarm_clock.ListAlarms.return_value = two_alarms + sub_callback(event=alarm_event) await hass.async_block_till_done() - entity_registry = async_get_entity_registry(hass) - assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" in entity_registry.entities - alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value alarm_event.increment_variable("alarm_list_version") + alarm_clock.ListAlarms.return_value = one_alarm + sub_callback(event=alarm_event) await hass.async_block_till_done() From a250343c55306d850360cd9d385ca0f6c6deaf87 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 16 Jun 2021 15:53:45 -0400 Subject: [PATCH 393/750] Support bitmask as a value (#51892) * Support bitmask as a value * Fix value schema and add tests * fix test * revert change to value schema --- homeassistant/components/zwave_js/api.py | 3 +- homeassistant/components/zwave_js/services.py | 24 +-- tests/components/zwave_js/test_api.py | 59 ++++++- tests/components/zwave_js/test_services.py | 158 ++++++++++++++++++ 4 files changed, 228 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5cf3ba32411..754fdfc25ab 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -51,6 +51,7 @@ from .const import ( EVENT_DEVICE_ADDED_TO_REGISTRY, ) from .helpers import async_enable_statistics, update_data_collection_preference +from .services import BITMASK_SCHEMA # general API constants ID = "id" @@ -925,7 +926,7 @@ async def websocket_refresh_node_cc_values( vol.Required(NODE_ID): int, vol.Required(PROPERTY): int, vol.Optional(PROPERTY_KEY): int, - vol.Required(VALUE): int, + vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA), } ) @websocket_api.async_response diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 930f002cf1e..47709a908ed 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -66,6 +66,14 @@ BITMASK_SCHEMA = vol.All( lambda value: int(value, 16), ) +VALUE_SCHEMA = vol.Any( + bool, + vol.Coerce(int), + vol.Coerce(float), + BITMASK_SCHEMA, + cv.string, +) + class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" @@ -177,7 +185,7 @@ class ZWaveServices: vol.Coerce(int), BITMASK_SCHEMA ), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string + vol.Coerce(int), BITMASK_SCHEMA, cv.string ), }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), @@ -204,7 +212,7 @@ class ZWaveServices: { vol.Any( vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) + ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string) }, ), }, @@ -224,7 +232,7 @@ class ZWaveServices: vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional( const.ATTR_REFRESH_ALL_VALUES, default=False - ): bool, + ): cv.boolean, }, validate_entities, ) @@ -250,10 +258,8 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, + vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean, }, cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), get_nodes_from_service_data, @@ -281,9 +287,7 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, }, vol.Any( cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index bb34193e2d1..f0b5a470df0 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1179,13 +1179,62 @@ async def test_set_config_parameter( client.async_send_command_no_wait.reset_mock() + # Test that hex strings are accepted and converted as expected + client.async_send_command_no_wait.return_value = None + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: "0x1", + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + with patch( "homeassistant.components.zwave_js.api.async_set_config_parameter", ) as set_param_mock: set_param_mock.side_effect = InvalidNewValue("test") await ws_client.send_json( { - ID: 2, + ID: 3, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1205,7 +1254,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = NotFoundError("test") await ws_client.send_json( { - ID: 3, + ID: 4, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1225,7 +1274,7 @@ async def test_set_config_parameter( set_param_mock.side_effect = SetValueFailed("test") await ws_client.send_json( { - ID: 4, + ID: 5, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, @@ -1245,7 +1294,7 @@ async def test_set_config_parameter( # Test getting non-existent node fails await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 9999, @@ -1264,7 +1313,7 @@ async def test_set_config_parameter( await ws_client.send_json( { - ID: 6, + ID: 7, TYPE: "zwave_js/set_config_parameter", ENTRY_ID: entry.entry_id, NODE_ID: 52, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index a7aea70f6a7..dfc7ddaa85d 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -89,6 +89,50 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): client.async_send_command_no_wait.reset_mock() + # Test setting config parameter value in hex + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: 1, + ATTR_CONFIG_VALUE: "0x1", + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyName": "Group 2: Send battery reports", + "propertyKey": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + # Test setting parameter by property name await hass.services.async_call( DOMAIN, @@ -419,6 +463,36 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati client.async_send_command_no_wait.reset_mock() + # Test using hex values for config parameter values + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: AIR_TEMPERATURE_SENSOR, + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: { + 1: "0x1", + 16: "0x1", + 32: "0x1", + 64: "0x1", + 128: "0x1", + }, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClass": 112, + "property": 102, + } + assert args["value"] == 241 + + client.async_send_command_no_wait.reset_mock() + await hass.services.async_call( DOMAIN, SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, @@ -535,6 +609,21 @@ async def test_poll_value( ) assert len(client.async_send_command.call_args_list) == 8 + client.async_send_command.reset_mock() + + # Test polling all watched values using string for boolean + client.async_send_command.return_value = {"result": 2} + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_REFRESH_ALL_VALUES: "true", + }, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 8 + # Test polling against an invalid entity raises MultipleInvalid with pytest.raises(vol.MultipleInvalid): await hass.services.async_call( @@ -586,6 +675,44 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): client.async_send_command_no_wait.reset_mock() + # Test bitmask as value and non bool as bool + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: "0x2", + ATTR_WAIT_FOR_RESULT: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 5 + assert args["valueId"] == { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "Local protection state", + "states": {"0": "Unprotected", "2": "NoOperationPossible"}, + }, + "value": 0, + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + # Test that when a command fails we raise an exception client.async_send_command.return_value = {"success": False} @@ -680,6 +807,37 @@ async def test_multicast_set_value( client.async_send_command.reset_mock() + # Test successful multicast call with hex value + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_DANFOSS_LC13_ENTITY, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: "0x2", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "multicast_group.set_value" + assert args["nodeIDs"] == [ + climate_radio_thermostat_ct100_plus_different_endpoints.node_id, + climate_danfoss_lc_13.node_id, + ] + assert args["valueId"] == { + "commandClass": 117, + "property": "local", + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + # Test successful broadcast call await hass.services.async_call( DOMAIN, From 17414439dfa27485ed486b91c618898eea7995b9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 17 Jun 2021 00:10:03 +0000 Subject: [PATCH 394/750] [ci skip] Translation update --- .../components/ambee/translations/he.json | 8 +++ .../components/wemo/translations/ca.json | 5 ++ .../components/wemo/translations/en.json | 5 ++ .../components/wemo/translations/et.json | 5 ++ .../components/wemo/translations/it.json | 5 ++ .../components/wemo/translations/nl.json | 5 ++ .../components/wemo/translations/pl.json | 5 ++ .../components/wemo/translations/ru.json | 5 ++ .../components/wemo/translations/zh-Hant.json | 5 ++ .../xiaomi_miio/translations/he.json | 12 +++- .../xiaomi_miio/translations/it.json | 58 ++++++++++++++++++- .../xiaomi_miio/translations/pl.json | 46 ++++++++++++++- .../yamaha_musiccast/translations/he.json | 17 ++++++ .../yamaha_musiccast/translations/it.json | 23 ++++++++ .../yamaha_musiccast/translations/pl.json | 14 ++++- 15 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/yamaha_musiccast/translations/he.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/it.json diff --git a/homeassistant/components/ambee/translations/he.json b/homeassistant/components/ambee/translations/he.json index 3337f1efaef..7b7882cd4df 100644 --- a/homeassistant/components/ambee/translations/he.json +++ b/homeassistant/components/ambee/translations/he.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/wemo/translations/ca.json b/homeassistant/components/wemo/translations/ca.json index 1216504eb85..ec27622fa83 100644 --- a/homeassistant/components/wemo/translations/ca.json +++ b/homeassistant/components/wemo/translations/ca.json @@ -9,5 +9,10 @@ "description": "Vols configurar Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Bot\u00f3 Wemo premut durant 2 segons" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/en.json b/homeassistant/components/wemo/translations/en.json index 20a9af468ff..e3c2b18c2ad 100644 --- a/homeassistant/components/wemo/translations/en.json +++ b/homeassistant/components/wemo/translations/en.json @@ -9,5 +9,10 @@ "description": "Do you want to set up Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo button was pressed for 2 seconds" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/et.json b/homeassistant/components/wemo/translations/et.json index e9959620510..d99f6bc1f57 100644 --- a/homeassistant/components/wemo/translations/et.json +++ b/homeassistant/components/wemo/translations/et.json @@ -9,5 +9,10 @@ "description": "Kas soovid Wemo-t seadistada?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo nuppu vajutati 2 sekundit" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/it.json b/homeassistant/components/wemo/translations/it.json index 691c7ca46b3..c1c66ac0ded 100644 --- a/homeassistant/components/wemo/translations/it.json +++ b/homeassistant/components/wemo/translations/it.json @@ -9,5 +9,10 @@ "description": "Vuoi configurare Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Il pulsante Wemo \u00e8 stato premuto per 2 secondi" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json index e4087863726..7fde5a7ef42 100644 --- a/homeassistant/components/wemo/translations/nl.json +++ b/homeassistant/components/wemo/translations/nl.json @@ -9,5 +9,10 @@ "description": "Wilt u Wemo instellen?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo-knop werd 2 seconden ingedrukt" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/pl.json b/homeassistant/components/wemo/translations/pl.json index a8ee3fa57ac..d8b06f79e48 100644 --- a/homeassistant/components/wemo/translations/pl.json +++ b/homeassistant/components/wemo/translations/pl.json @@ -9,5 +9,10 @@ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Przycisk Wemo zosta\u0142 wci\u015bni\u0119ty przez 2 sekundy" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ru.json b/homeassistant/components/wemo/translations/ru.json index 60b1efb3dea..91994b113de 100644 --- a/homeassistant/components/wemo/translations/ru.json +++ b/homeassistant/components/wemo/translations/ru.json @@ -9,5 +9,10 @@ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 Wemo \u0431\u044b\u043b\u0430 \u043d\u0430\u0436\u0430\u0442\u0430 \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 2 \u0441\u0435\u043a\u0443\u043d\u0434" + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json index 4be83508478..a9a4a2a8b20 100644 --- a/homeassistant/components/wemo/translations/zh-Hant.json +++ b/homeassistant/components/wemo/translations/zh-Hant.json @@ -9,5 +9,10 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Wemo\uff1f" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo \u6309\u9215\u6309\u4e0b 2 \u79d2" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index f7f60416ade..9eb4ffc0bb7 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" @@ -21,6 +22,15 @@ "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara." + }, + "manual": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 8eb851cda5d..ba53ea170fe 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso" + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "incomplete_info": "Informazioni incomplete per configurare il dispositivo, nessun host o token fornito.", + "not_xiaomi_miio": "Il dispositivo non \u00e8 (ancora) supportato da Xiaomi Miio.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", + "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese", + "cloud_login_error": "Impossibile accedere a Xioami Miio Cloud, controlla le credenziali.", + "cloud_no_devices": "Nessun dispositivo trovato in questo account cloud Xiaomi Miio.", "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Paese del server cloud", + "cloud_password": "Password cloud", + "cloud_username": "Nome utente cloud", + "manual": "Configura manualmente (non consigliato)" + }, + "description": "Accedi al cloud Xiaomi Miio, vedi https://www.openhab.org/addons/bindings/miio/#country-servers per utilizzare il server cloud.", + "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Modello del dispositivo" + }, + "description": "Seleziona manualmente il modello del dispositivo tra i modelli supportati.", + "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, "device": { "data": { "host": "Indirizzo IP", @@ -30,6 +53,25 @@ "description": "E' necessaria la Token API di 32 caratteri, vedere https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Notare che questa Token API \u00e8 differente dalla chiave usata dall'integrazione di Xiaomi Aqara.", "title": "Connessione a un Xiaomi Gateway " }, + "manual": { + "data": { + "host": "Indirizzo IP", + "token": "Token API" + }, + "description": "Avrai bisogno dei 32 caratteri del Token API, vedi https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token per istruzioni. Tieni presente che questo Token API \u00e8 diverso dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", + "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "L'integrazione di Xiaomi Miio deve riautenticare il tuo account per aggiornare i token o aggiungere credenziali cloud mancanti.", + "title": "Autenticare nuovamente l'integrazione" + }, + "select": { + "data": { + "select_device": "Dispositivo Miio" + }, + "description": "Seleziona il dispositivo Xiaomi Miio da configurare.", + "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + }, "user": { "data": { "gateway": "Connettiti a un Xiaomi Gateway" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Usa il cloud per connettere i sottodispositivi" + }, + "description": "Specificare le impostazioni opzionali", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 9051e9de3d9..dacb0f3f3ec 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -3,15 +3,37 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "incomplete_info": "Niepe\u0142ne informacje do skonfigurowania urz\u0105dzenia, brak nazwy hosta, IP lub tokena.", + "not_xiaomi_miio": "Urz\u0105dzenie nie jest (jeszcze) obs\u0142ugiwane przez Xiaomi Miio.", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "cloud_credentials_incomplete": "Dane logowania do chmury niekompletne, prosz\u0119 poda\u0107 nazw\u0119 u\u017cytkownika, has\u0142o i kraj", + "cloud_login_error": "Nie mo\u017cna zalogowa\u0107 si\u0119 do chmury Xioami Miio, sprawd\u017a po\u015bwiadczenia.", + "cloud_no_devices": "Na tym koncie Xiaomi Miio nie znaleziono \u017cadnych urz\u0105dze\u0144.", "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Kraj serwera w chmurze", + "cloud_password": "Has\u0142o do chmury", + "cloud_username": "Nazwa u\u017cytkownika do chmury", + "manual": "Skonfiguruj r\u0119cznie (niezalecane)" + }, + "description": "Zaloguj si\u0119 do chmury Xiaomi Miio, zobacz https://www.openhab.org/addons/bindings/miio/#country-servers, aby zobaczy\u0107, kt\u00f3rego serwera u\u017cy\u0107.", + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" + }, + "connect": { + "data": { + "model": "Model urz\u0105dzenia" + }, + "description": "Wybierz r\u0119cznie model urz\u0105dzenia z listy obs\u0142ugiwanych modeli.", + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" + }, "device": { "data": { "host": "Adres IP", @@ -20,7 +42,7 @@ "token": "Token API" }, "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi b\u0105d\u017a innym urz\u0105dzeniem Xiaomi Miio" + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" }, "gateway": { "data": { @@ -35,7 +57,20 @@ "data": { "host": "Adres IP", "token": "Token API" - } + }, + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32-znaki), zobacz https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Pami\u0119taj, \u017ce ten token API r\u00f3\u017cni si\u0119 od klucza u\u017cywanego przez integracj\u0119 Xiaomi Aqara.", + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" + }, + "reauth_confirm": { + "description": "Integracja Xiaomi Miio wymaga ponownego uwierzytelnienia Twoje konta, aby zaktualizowa\u0107 tokeny lub doda\u0107 brakuj\u0105ce dane uwierzytelniaj\u0105ce do chmury.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "select": { + "data": { + "select_device": "Urz\u0105dzenie Miio" + }, + "description": "Wybierz urz\u0105dzenie Xiaomi Miio do skonfigurowania.", + "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" }, "user": { "data": { @@ -47,8 +82,15 @@ } }, "options": { + "error": { + "cloud_credentials_incomplete": "Dane logowania do chmury niekompletne, prosz\u0119 poda\u0107 nazw\u0119 u\u017cytkownika, has\u0142o i kraj" + }, "step": { "init": { + "data": { + "cloud_subdevices": "U\u017cyj chmury, aby uzyska\u0107 pod\u0142\u0105czone podurz\u0105dzenia" + }, + "description": "Ustawienia opcjonalne", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/yamaha_musiccast/translations/he.json b/homeassistant/components/yamaha_musiccast/translations/he.json new file mode 100644 index 00000000000..8c8484a1160 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/it.json b/homeassistant/components/yamaha_musiccast/translations/it.json new file mode 100644 index 00000000000..09a9e698ab5 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "yxc_control_url_missing": "L'URL di controllo non \u00e8 fornito nella descrizione ssdp." + }, + "error": { + "no_musiccast_device": "Questo dispositivo sembra non essere un dispositivo MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configura MusicCast per l'integrazione con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json index 4d32dce32f9..d22126557d0 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/pl.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "yxc_control_url_missing": "Kontrolny adres URL nie jest podany w opisie ssdp." + }, + "error": { + "no_musiccast_device": "Wygl\u0105da na to, \u017ce to urz\u0105dzenie nie jest urz\u0105dzeniem MusicCast." + }, + "flow_title": "MusicCast: {name}", "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" - } + }, + "description": "Skonfiguruj MusicCast, aby zintegrowa\u0107 go z Home Assistantem." } } } From 33e08f38da9db3998ea0412ad4b160d99006c437 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 04:41:19 +0200 Subject: [PATCH 395/750] Raise bad request when receiving HTTP request from untrusted proxy (#51839) * Raise bad request when receiving HTTP request from untrusted proxy * Fix code comment --- homeassistant/components/http/forwarded.py | 21 +++--- tests/components/http/test_forwarded.py | 79 +--------------------- 2 files changed, 11 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 5c62a469924..18bc51af1d1 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -47,7 +47,8 @@ def async_setup_forwarded( Additionally: - If no X-Forwarded-For header is found, the processing of all headers is skipped. - - Log a warning when untrusted connected peer provides X-Forwarded-For headers. + - Throw HTTP 400 status when untrusted connected peer provides + X-Forwarded-For headers. - If multiple instances of X-Forwarded-For, X-Forwarded-Proto or X-Forwarded-Host are found, an HTTP 400 status code is thrown. - If malformed or invalid (IP) data in X-Forwarded-For header is found, @@ -87,26 +88,20 @@ def async_setup_forwarded( # We have X-Forwarded-For, but config does not agree if not use_x_forwarded_for: - _LOGGER.warning( + _LOGGER.error( "A request from a reverse proxy was received from %s, but your " - "HTTP integration is not set-up for reverse proxies; " - "This request will be blocked in Home Assistant 2021.7 unless " - "you configure your HTTP integration to allow this header", + "HTTP integration is not set-up for reverse proxies", connected_ip, ) - # Block this request in the future, for now we pass. - return await handler(request) + raise HTTPBadRequest # Ensure the IP of the connected peer is trusted if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): - _LOGGER.warning( - "Received X-Forwarded-For header from untrusted proxy %s, headers not processed; " - "This request will be blocked in Home Assistant 2021.7 unless you configure " - "your HTTP integration to allow this proxy to reverse your Home Assistant instance", + _LOGGER.error( + "Received X-Forwarded-For header from an untrusted proxy %s", connected_ip, ) - # Not trusted, Block this request in the future, continue as normal - return await handler(request) + raise HTTPBadRequest # Multiple X-Forwarded-For headers if len(forwarded_for_headers) > 1: diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 4b7a3421b0a..400a1f32729 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -33,9 +33,9 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 + assert resp.status == 400 assert ( - "Received X-Forwarded-For header from untrusted proxy 127.0.0.1, headers not processed" + "Received X-Forwarded-For header from an untrusted proxy 127.0.0.1" in caplog.text ) @@ -103,35 +103,13 @@ async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 + assert resp.status == 400 assert ( "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " "integration is not set-up for reverse proxies" in caplog.text ) -async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): - """Test that we get the IP from transport with untrusted proxy.""" - - async def handler(request): - url = mock_api_client.make_url("/") - assert request.host == f"{url.host}:{url.port}" - assert request.scheme == "http" - assert not request.secure - assert request.remote == "127.0.0.1" - - return web.Response() - - app = web.Application() - app.router.add_get("/", handler) - async_setup_forwarded(app, True, [ip_network("1.1.1.1")]) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - - assert resp.status == 200 - - async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): """Test that we get the IP from the transport with a spoofed header.""" @@ -205,31 +183,6 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): assert "Too many headers for X-Forwarded-For" in caplog.text -async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): - """Test that proto header is ignored when untrusted.""" - - async def handler(request): - url = mock_api_client.make_url("/") - assert request.host == f"{url.host}:{url.port}" - assert request.scheme == "http" - assert not request.secure - assert request.remote == "127.0.0.1" - - return web.Response() - - app = web.Application() - app.router.add_get("/", handler) - - async_setup_forwarded(app, True, []) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get( - "/", headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_PROTO: "https"} - ) - - assert resp.status == 200 - - @pytest.mark.parametrize( "x_forwarded_for,remote,x_forwarded_proto,secure", [ @@ -409,32 +362,6 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( ) -async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): - """Test that host header is ignored when untrusted.""" - - async def handler(request): - url = mock_api_client.make_url("/") - assert request.host == f"{url.host}:{url.port}" - assert request.scheme == "http" - assert not request.secure - assert request.remote == "127.0.0.1" - - return web.Response() - - app = web.Application() - app.router.add_get("/", handler) - - async_setup_forwarded(app, True, []) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get( - "/", - headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_HOST: "example.com"}, - ) - - assert resp.status == 200 - - async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): """Test that we get the host header if proxy is trusted.""" From 986c4a8f29043216bdc58b1a7f219b9388c35aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Kr=C3=B3l?= Date: Thu, 17 Jun 2021 05:24:17 +0200 Subject: [PATCH 396/750] Support Wolflink reconnection after unexpected failure (#47011) * Support reconnection after unexpected fails * Update session at every call. Support Offline devices * Remove unnecessary else branch * Clean typing Co-authored-by: Martin Hjelmare --- homeassistant/components/wolflink/__init__.py | 53 +++++++++++++++---- .../components/wolflink/manifest.json | 2 +- homeassistant/components/wolflink/sensor.py | 6 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 7286f28568d..ab078e438c6 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -4,7 +4,7 @@ import logging from httpx import ConnectError, ConnectTimeout from wolf_smartset.token_auth import InvalidAuth -from wolf_smartset.wolf_client import FetchFailed, WolfClient +from wolf_smartset.wolf_client import FetchFailed, ParameterReadError, WolfClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -33,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_name = entry.data[DEVICE_NAME] device_id = entry.data[DEVICE_ID] gateway_id = entry.data[DEVICE_GATEWAY] + refetch_parameters = False _LOGGER.debug( "Setting up wolflink integration for device: %s (ID: %s, gateway: %s)", device_name, @@ -42,17 +43,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: wolf_client = WolfClient(username, password) - try: - parameters = await fetch_parameters(wolf_client, gateway_id, device_id) - except InvalidAuth: - _LOGGER.debug("Authentication failed") - return False + parameters = await fetch_parameters_init(wolf_client, gateway_id, device_id) async def async_update_data(): """Update all stored entities for Wolf SmartSet.""" try: - values = await wolf_client.fetch_value(gateway_id, device_id, parameters) - return {v.value_id: v.value for v in values} + nonlocal refetch_parameters + nonlocal parameters + await wolf_client.update_session() + if not wolf_client.fetch_system_state_list(device_id, gateway_id): + refetch_parameters = True + raise UpdateFailed( + "Could not fetch values from server because device is Offline." + ) + if refetch_parameters: + parameters = await fetch_parameters(wolf_client, gateway_id, device_id) + hass.data[DOMAIN][entry.entry_id][PARAMETERS] = parameters + refetch_parameters = False + values = { + v.value_id: v.value + for v in await wolf_client.fetch_value( + gateway_id, device_id, parameters + ) + } + return { + parameter.parameter_id: ( + parameter.value_id, + values[parameter.value_id], + ) + for parameter in parameters + if parameter.value_id in values + } except ConnectError as exception: raise UpdateFailed( f"Error communicating with API: {exception}" @@ -61,13 +82,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed( f"Could not fetch values from server due to: {exception}" ) from exception + except ParameterReadError as exception: + refetch_parameters = True + raise UpdateFailed( + "Could not fetch values for parameter. Refreshing value IDs." + ) from exception except InvalidAuth as exception: raise UpdateFailed("Invalid authentication during update.") from exception coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="wolflink", + name=DOMAIN, update_method=async_update_data, update_interval=timedelta(minutes=1), ) @@ -100,9 +126,14 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int): By default Reglertyp entity is removed because API will not provide value for this parameter. """ + fetched_parameters = await client.fetch_parameters(gateway_id, device_id) + return [param for param in fetched_parameters if param.name != "Reglertyp"] + + +async def fetch_parameters_init(client: WolfClient, gateway_id: int, device_id: int): + """Fetch all available parameters with usage of WolfClient but handles all exceptions and results in ConfigEntryNotReady.""" try: - fetched_parameters = await client.fetch_parameters(gateway_id, device_id) - return [param for param in fetched_parameters if param.name != "Reglertyp"] + return await fetch_parameters(client, gateway_id, device_id) except (ConnectError, ConnectTimeout, FetchFailed) as exception: raise ConfigEntryNotReady( f"Error communicating with API: {exception}" diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 504419ef0f4..749f7bbc67c 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -3,7 +3,7 @@ "name": "Wolf SmartSet Service", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wolflink", - "requirements": ["wolf_smartset==0.1.8"], + "requirements": ["wolf_smartset==0.1.11"], "codeowners": ["@adamkrol93"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index f243160ff59..0d35d4bce5c 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -65,8 +65,10 @@ class WolfLinkSensor(CoordinatorEntity, SensorEntity): @property def state(self): """Return the state. Wolf Client is returning only changed values so we need to store old value here.""" - if self.wolf_object.value_id in self.coordinator.data: - self._state = self.coordinator.data[self.wolf_object.value_id] + if self.wolf_object.parameter_id in self.coordinator.data: + new_state = self.coordinator.data[self.wolf_object.parameter_id] + self.wolf_object.value_id = new_state[0] + self._state = new_state[1] return self._state @property diff --git a/requirements_all.txt b/requirements_all.txt index 75937cd3439..3e398156dc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2368,7 +2368,7 @@ withings-api==2.3.2 wled==0.6.0 # homeassistant.components.wolflink -wolf_smartset==0.1.8 +wolf_smartset==0.1.11 # homeassistant.components.xbee xbee-helper==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47bd108f6ca..7e982626fe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1286,7 +1286,7 @@ withings-api==2.3.2 wled==0.6.0 # homeassistant.components.wolflink -wolf_smartset==0.1.8 +wolf_smartset==0.1.11 # homeassistant.components.xbox xbox-webapi==2.0.11 From c8329032b210554a09841ea12c225117aece1156 Mon Sep 17 00:00:00 2001 From: Konstantin Antselovich Date: Wed, 16 Jun 2021 20:57:46 -0700 Subject: [PATCH 397/750] Fix whois expiration date (#51868) --- homeassistant/components/whois/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6d97037a4ee..4219c80193d 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -118,6 +118,7 @@ class WhoisSensor(SensorEntity): expiration_date = response["expiration_date"] if isinstance(expiration_date, list): attrs[ATTR_EXPIRES] = expiration_date[0].isoformat() + expiration_date = expiration_date[0] else: attrs[ATTR_EXPIRES] = expiration_date.isoformat() From fe26c34e87b3c20845920b489b1e8d4ada7c4ecd Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Thu, 17 Jun 2021 08:30:19 +0200 Subject: [PATCH 398/750] Clean ezviz error handling in services (#51945) --- homeassistant/components/ezviz/camera.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index d2dbd1b6aab..b09e5cdd901 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -290,8 +290,7 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): self.coordinator.ezviz_client.set_camera_defence(self._serial, 1) except InvalidHost as err: - _LOGGER.error("Error enabling motion detection") - raise InvalidHost from err + raise InvalidHost("Error enabling motion detection") from err def disable_motion_detection(self): """Disable motion detection.""" @@ -299,8 +298,7 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): self.coordinator.ezviz_client.set_camera_defence(self._serial, 0) except InvalidHost as err: - _LOGGER.error("Error disabling motion detection") - raise InvalidHost from err + raise InvalidHost("Error disabling motion detection") from err @property def unique_id(self): @@ -354,32 +352,30 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): ) except HTTPError as err: - _LOGGER.error("Cannot perform PTZ") - raise HTTPError from err + raise HTTPError("Cannot perform PTZ") from err def perform_sound_alarm(self, enable): """Sound the alarm on a camera.""" try: self.coordinator.ezviz_client.sound_alarm(self._serial, enable) except HTTPError as err: - _LOGGER.debug("Cannot sound alarm") - raise HTTPError from err + raise HTTPError("Cannot sound alarm") from err def perform_wake_device(self): """Basically wakes the camera by querying the device.""" try: self.coordinator.ezviz_client.get_detection_sensibility(self._serial) except (HTTPError, PyEzvizError) as err: - _LOGGER.error("Cannot wake device") - raise PyEzvizError from err + raise PyEzvizError("Cannot wake device") from err def perform_alarm_sound(self, level): """Enable/Disable movement sound alarm.""" try: self.coordinator.ezviz_client.alarm_sound(self._serial, level, 1) except HTTPError as err: - _LOGGER.error("Cannot set alarm sound level for on movement detected") - raise HTTPError from err + raise HTTPError( + "Cannot set alarm sound level for on movement detected" + ) from err def perform_set_alarm_detection_sensibility(self, level, type_value): """Set camera detection sensibility level service.""" @@ -388,5 +384,4 @@ class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): self._serial, level, type_value ) except (HTTPError, PyEzvizError) as err: - _LOGGER.error("Cannot set detection sensitivity level") - raise PyEzvizError from err + raise PyEzvizError("Cannot set detection sensitivity level") from err From aaf3a5a9c5940df486d18dba894f2a43d6d3fcd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Jun 2021 08:31:20 +0200 Subject: [PATCH 399/750] Bump actions/upload-artifact from 2.2.3 to 2.2.4 (#51946) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.3 to 2.2.4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2.2.3...v2.2.4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f2242dd90d..139fa614597 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -700,7 +700,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.3 + uses: actions/upload-artifact@v2.2.4 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 02ae3ccbbd0..1e12c8a1dff 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -45,13 +45,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v2.2.4 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v2.2.4 with: name: requirements_diff path: ./requirements_diff.txt From d4ac5bf04834949790c43984955e9cd283668fc5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Jun 2021 02:02:23 -0500 Subject: [PATCH 400/750] Bump plexapi to 4.6.1 (#51936) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plex/test_browse_media.py | 11 ++++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 5d6ffd19550..3de7895a805 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.5.1", + "plexapi==4.6.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3e398156dc7..866ecd62814 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1166,7 +1166,7 @@ pillow==8.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.5.1 +plexapi==4.6.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e982626fe5..326e4bbc667 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -639,7 +639,7 @@ pilight==0.1.1 pillow==8.2.0 # homeassistant.components.plex -plexapi==4.5.1 +plexapi==4.6.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index d9f02c49341..4892262fc32 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -11,7 +11,12 @@ from .const import DEFAULT_DATA async def test_browse_media( - hass, hass_ws_client, mock_plex_server, requests_mock, library_movies_filtertypes + hass, + hass_ws_client, + mock_plex_server, + requests_mock, + library_movies_filtertypes, + empty_payload, ): """Test getting Plex clients from plex.tv.""" websocket_client = await hass_ws_client(hass) @@ -92,6 +97,10 @@ async def test_browse_media( f"{mock_plex_server.url_in_use}/library/sections/1/all?includeMeta=1", text=library_movies_filtertypes, ) + requests_mock.get( + f"{mock_plex_server.url_in_use}/library/sections/1/collections?includeMeta=1", + text=empty_payload, + ) msg_id += 1 library_section_id = next(iter(mock_plex_server.library.sections())).key From b7c1df7864468ddc4b9a2bec51894c7e25fb0104 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Thu, 17 Jun 2021 09:03:28 +0200 Subject: [PATCH 401/750] Adopt new electricity tariffs in pvpc hourly pricing (#51789) --- .../pvpc_hourly_pricing/__init__.py | 101 ++- .../pvpc_hourly_pricing/config_flow.py | 51 +- .../components/pvpc_hourly_pricing/const.py | 5 +- .../pvpc_hourly_pricing/manifest.json | 2 +- .../components/pvpc_hourly_pricing/sensor.py | 48 +- .../pvpc_hourly_pricing/strings.json | 21 +- .../pvpc_hourly_pricing/translations/en.json | 21 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../pvpc_hourly_pricing/conftest.py | 7 + .../pvpc_hourly_pricing/test_config_flow.py | 56 +- .../pvpc_hourly_pricing/test_sensor.py | 89 ++- .../PVPC_CURV_DD_2021_06_01.json | 604 ++++++++++++++++++ 13 files changed, 938 insertions(+), 71 deletions(-) create mode 100644 tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 22ad590659e..3e98274c696 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,17 +1,38 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" +import logging + +from aiopvpc import DEFAULT_POWER_KW, TARIFFS import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + async_get, + async_migrate_entries, +) -from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORMS, TARIFFS +from .const import ( + ATTR_POWER, + ATTR_POWER_P3, + ATTR_TARIFF, + DEFAULT_NAME, + DOMAIN, + PLATFORMS, +) +_LOGGER = logging.getLogger(__name__) +_DEFAULT_TARIFF = TARIFFS[0] +VALID_POWER = vol.All(vol.Coerce(float), vol.Range(min=1.0, max=15.0)) +VALID_TARIFF = vol.In(TARIFFS) UI_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(ATTR_TARIFF, default=DEFAULT_TARIFF): vol.In(TARIFFS), + vol.Required(ATTR_TARIFF, default=_DEFAULT_TARIFF): VALID_TARIFF, + vol.Required(ATTR_POWER, default=DEFAULT_POWER_KW): VALID_POWER, + vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER, } ) CONFIG_SCHEMA = vol.Schema( @@ -20,19 +41,8 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): - """ - Set up the electricity price sensor from configuration.yaml. - - ```yaml - pvpc_hourly_pricing: - - name: PVPC manual ve - tariff: electric_car - - name: PVPC manual nocturna - tariff: discrimination - timeout: 3 - ``` - """ +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the electricity price sensor from configuration.yaml.""" for conf in config.get(DOMAIN, []): hass.async_create_task( hass.config_entries.flow.async_init( @@ -45,10 +55,67 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pvpc hourly pricing from a config entry.""" + if len(entry.data) == 2: + defaults = { + ATTR_TARIFF: _DEFAULT_TARIFF, + ATTR_POWER: DEFAULT_POWER_KW, + ATTR_POWER_P3: DEFAULT_POWER_KW, + } + data = {**entry.data, **defaults} + hass.config_entries.async_update_entry( + entry, unique_id=_DEFAULT_TARIFF, data=data, options=defaults + ) + + @callback + def update_unique_id(reg_entry): + """Change unique id for sensor entity, pointing to new tariff.""" + return {"new_unique_id": _DEFAULT_TARIFF} + + try: + await async_migrate_entries(hass, entry.entry_id, update_unique_id) + _LOGGER.warning( + "Migrating PVPC sensor from old tariff '%s' to new '%s'. " + "Configure the integration to set your contracted power, " + "and select prices for Ceuta/Melilla, " + "if that is your case", + entry.data[ATTR_TARIFF], + _DEFAULT_TARIFF, + ) + except ValueError: + # there were multiple sensors (with different old tariffs, up to 3), + # so we leave just one and remove the others + ent_reg: EntityRegistry = async_get(hass) + for entity_id, reg_entry in ent_reg.entities.items(): + if reg_entry.config_entry_id == entry.entry_id: + ent_reg.async_remove(entity_id) + _LOGGER.warning( + "Old PVPC Sensor %s is removed " + "(another one already exists, using the same tariff)", + entity_id, + ) + break + + await hass.config_entries.async_remove(entry.entry_id) + return False + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_options)) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + if any( + entry.data.get(attrib) != entry.options.get(attrib) + for attrib in (ATTR_TARIFF, ATTR_POWER, ATTR_POWER_P3) + ): + # update entry replacing data with new options + hass.config_entries.async_update_entry( + entry, data={**entry.data, **entry.options} + ) + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 971a13acc2f..76694d570b5 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -1,17 +1,24 @@ """Config flow for pvpc_hourly_pricing.""" +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.core import callback -from . import CONF_NAME, UI_CONFIG_SCHEMA -from .const import ATTR_TARIFF, DOMAIN - -_DOMAIN_NAME = DOMAIN +from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER, VALID_TARIFF +from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN -class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME): - """Handle a config flow for `pvpc_hourly_pricing` to select the tariff.""" +class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle config flow for `pvpc_hourly_pricing`.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return PVPCOptionsFlowHandler(config_entry) + async def async_step_user(self, user_input=None): """Handle the initial step.""" if user_input is not None: @@ -24,3 +31,35 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME): async def async_step_import(self, import_info): """Handle import from config file.""" return await self.async_step_user(import_info) + + +class PVPCOptionsFlowHandler(config_entries.OptionsFlow): + """Handle PVPC options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + # Fill options with entry data + tariff = self.config_entry.options.get( + ATTR_TARIFF, self.config_entry.data[ATTR_TARIFF] + ) + power = self.config_entry.options.get( + ATTR_POWER, self.config_entry.data[ATTR_POWER] + ) + power_valley = self.config_entry.options.get( + ATTR_POWER_P3, self.config_entry.data[ATTR_POWER_P3] + ) + schema = vol.Schema( + { + vol.Required(ATTR_TARIFF, default=tariff): VALID_TARIFF, + vol.Required(ATTR_POWER, default=power): VALID_POWER, + vol.Required(ATTR_POWER_P3, default=power_valley): VALID_POWER, + } + ) + return self.async_show_form(step_id="init", data_schema=schema) diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index 9e11bc57d6d..ad97124c330 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -1,8 +1,7 @@ """Constant values for pvpc_hourly_pricing.""" -from aiopvpc import TARIFFS - DOMAIN = "pvpc_hourly_pricing" PLATFORMS = ["sensor"] +ATTR_POWER = "power" +ATTR_POWER_P3 = "power_p3" ATTR_TARIFF = "tariff" DEFAULT_NAME = "PVPC" -DEFAULT_TARIFF = TARIFFS[1] diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index bbbe18350c8..612376a7931 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.1.2"], + "requirements": ["aiopvpc==2.2.0"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 5fe65e3dc65..75881f93f0a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -3,19 +3,21 @@ from __future__ import annotations import logging from random import randint +from typing import Any from aiopvpc import PVPCData -from homeassistant import config_entries from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later, async_track_time_change from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util -from .const import ATTR_TARIFF +from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF _LOGGER = logging.getLogger(__name__) @@ -27,15 +29,18 @@ _DEFAULT_TIMEOUT = 10 async def async_setup_entry( - hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the electricity price sensor from config_entry.""" name = config_entry.data[CONF_NAME] pvpc_data_handler = PVPCData( tariff=config_entry.data[ATTR_TARIFF], + power=config_entry.data[ATTR_POWER], + power_valley=config_entry.data[ATTR_POWER_P3], local_timezone=hass.config.time_zone, websession=async_get_clientsession(hass), - logger=_LOGGER, timeout=_DEFAULT_TIMEOUT, ) async_add_entities( @@ -57,15 +62,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): self._pvpc_data = pvpc_data_handler self._num_retries = 0 - self._hourly_tracker = None - self._price_tracker = None - - async def async_will_remove_from_hass(self) -> None: - """Cancel listeners for sensor updates.""" - self._hourly_tracker() - self._price_tracker() - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() @@ -73,14 +70,18 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): self._pvpc_data.state = state.state # Update 'state' value in hour changes - self._hourly_tracker = async_track_time_change( - self.hass, self.update_current_price, second=[0], minute=[0] + self.async_on_remove( + async_track_time_change( + self.hass, self.update_current_price, second=[0], minute=[0] + ) ) # Update prices at random time, 2 times/hour (don't want to upset API) random_minute = randint(1, 29) mins_update = [random_minute, random_minute + 30] - self._price_tracker = async_track_time_change( - self.hass, self.async_update_prices, second=[0], minute=mins_update + self.async_on_remove( + async_track_time_change( + self.hass, self.async_update_prices, second=[0], minute=mins_update + ) ) _LOGGER.debug( "Setup of price sensor %s (%s) with tariff '%s', " @@ -90,8 +91,9 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): self._pvpc_data.tariff, mins_update, ) - await self.async_update_prices(dt_util.utcnow()) - self.update_current_price(dt_util.utcnow()) + now = dt_util.utcnow() + await self.async_update_prices(now) + self.update_current_price(now) @property def unique_id(self) -> str | None: @@ -99,12 +101,12 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): return self._unique_id @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def state(self): + def state(self) -> float: """Return the state of the sensor.""" return self._pvpc_data.state @@ -114,7 +116,7 @@ class ElecPriceSensor(RestoreEntity, SensorEntity): return self._pvpc_data.state_available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return self._pvpc_data.attributes diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index a1536d2186f..89da917c8ea 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -2,16 +2,31 @@ "config": { "step": { "user": { - "title": "Tariff selection", - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)", + "title": "Sensor setup", + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "data": { "name": "Sensor Name", - "tariff": "Contracted tariff (1, 2, or 3 periods)" + "tariff": "Applicable tariff by geographic zone", + "power": "Contracted power (kW)", + "power_p3": "Contracted power for valley period P3 (kW)" } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "options": { + "step": { + "init": { + "title": "Sensor setup", + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "data": { + "tariff": "Applicable tariff by geographic zone", + "power": "Contracted power (kW)", + "power_p3": "Contracted power for valley period P3 (kW)" + } + } + } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 02acb46eeb6..38f45d36ab6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Sensor Name", - "tariff": "Contracted tariff (1, 2, or 3 periods)" + "power": "Contracted power (kW)", + "power_p3": "Contracted power for valley period P3 (kW)", + "tariff": "Applicable tariff by geographic zone" }, - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)", - "title": "Tariff selection" + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Sensor setup" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Contracted power (kW)", + "power_p3": "Contracted power for valley period P3 (kW)", + "tariff": "Applicable tariff by geographic zone" + }, + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Sensor setup" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 866ecd62814..3ba957f69c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.2 +aiopvpc==2.2.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 326e4bbc667..e82fa369708 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.1.2 +aiopvpc==2.2.0 # homeassistant.components.webostv aiopylgtv==0.4.0 diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index a16923d0a73..2421c753518 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -14,6 +14,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker FIXTURE_JSON_DATA_2019_10_26 = "PVPC_CURV_DD_2019_10_26.json" FIXTURE_JSON_DATA_2019_10_27 = "PVPC_CURV_DD_2019_10_27.json" FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json" +FIXTURE_JSON_DATA_2021_06_01 = "PVPC_CURV_DD_2021_06_01.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): @@ -60,4 +61,10 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_29}"), ) + # new format for prices >= 2021-06-01 + aioclient_mock.get( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2021-06-01", + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}"), + ) + return aioclient_mock diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 2a64d81ef98..1c9cb7e133d 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -3,7 +3,13 @@ from datetime import datetime from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN +from homeassistant.components.pvpc_hourly_pricing import ( + ATTR_POWER, + ATTR_POWER_P3, + ATTR_TARIFF, + DOMAIN, + TARIFFS, +) from homeassistant.const import CONF_NAME from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -20,13 +26,20 @@ async def test_config_flow( """ Test config flow for pvpc_hourly_pricing. - - Create a new entry with tariff "normal" + - Create a new entry with tariff "2.0TD (Ceuta/Melilla)" - Check state and attributes - Check abort when trying to config another with same tariff - Check removal and add again to check state restoration + - Configure options to change power and tariff to "2.0TD" """ hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") - mock_data = {"return_time": datetime(2019, 10, 26, 14, 0, tzinfo=date_util.UTC)} + tst_config = { + CONF_NAME: "test", + ATTR_TARIFF: TARIFFS[1], + ATTR_POWER: 4.6, + ATTR_POWER_P3: 5.75, + } + mock_data = {"return_time": datetime(2021, 6, 1, 12, 0, tzinfo=date_util.UTC)} def mock_now(): return mock_data["return_time"] @@ -38,13 +51,13 @@ async def test_config_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + result["flow_id"], tst_config ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") - check_valid_state(state, tariff="normal") + check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 1 # Check abort when configuring another with same tariff @@ -53,7 +66,7 @@ async def test_config_flow( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + result["flow_id"], tst_config ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert pvpc_aioclient_mock.call_count == 1 @@ -70,11 +83,38 @@ async def test_config_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + result["flow_id"], tst_config ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") - check_valid_state(state, tariff="normal") + check_valid_state(state, tariff=TARIFFS[1]) + price_pbc = state.state assert pvpc_aioclient_mock.call_count == 2 + assert state.attributes["period"] == "P2" + assert state.attributes["next_period"] == "P1" + assert state.attributes["available_power"] == 4600 + + # check options flow + current_entries = hass.config_entries.async_entries(DOMAIN) + assert len(current_entries) == 1 + config_entry = current_entries[0] + + 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"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ATTR_TARIFF: TARIFFS[0], ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + price_cym = state.state + check_valid_state(state, tariff=TARIFFS[0]) + assert pvpc_aioclient_mock.call_count == 3 + assert state.attributes["period"] == "P2" + assert state.attributes["next_period"] == "P1" + assert state.attributes["available_power"] == 3000 + assert price_cym < price_pbc diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 19f3a7aa31c..cee2374f192 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -3,15 +3,20 @@ from datetime import datetime, timedelta import logging from unittest.mock import patch -from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN +from homeassistant.components.pvpc_hourly_pricing import ( + ATTR_POWER, + ATTR_POWER_P3, + ATTR_TARIFF, + DOMAIN, + TARIFFS, +) from homeassistant.const import CONF_NAME from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .conftest import check_valid_state -from tests.common import date_util +from tests.common import MockConfigEntry, date_util, mock_registry from tests.test_util.aiohttp import AiohttpClientMocker @@ -32,14 +37,27 @@ async def test_sensor_availability( ): """Test sensor availability and handling of cloud access.""" hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") - config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]} + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"} + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} def mock_now(): return mock_data["return_time"] with patch("homeassistant.util.dt.utcnow", new=mock_now): - assert await async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(config_entry.entry_id) + + # check migration + current_entries = hass.config_entries.async_entries(DOMAIN) + assert len(current_entries) == 1 + migrated_entry = current_entries[0] + assert migrated_entry.version == 1 + assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] + assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] + await hass.async_block_till_done() caplog.clear() assert pvpc_aioclient_mock.call_count == 2 @@ -85,3 +103,64 @@ async def test_sensor_availability( assert pvpc_aioclient_mock.call_count == 33 assert len(caplog.messages) == 1 assert caplog.records[0].levelno == logging.WARNING + + +async def test_multi_sensor_migration( + hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker +): + """Test tariff migration when there are >1 old sensors.""" + entity_reg = mock_registry(hass) + hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") + uid_1 = "discrimination" + uid_2 = "normal" + old_conf_1 = {CONF_NAME: "test_pvpc_1", ATTR_TARIFF: uid_1} + old_conf_2 = {CONF_NAME: "test_pvpc_2", ATTR_TARIFF: uid_2} + + config_entry_1 = MockConfigEntry(domain=DOMAIN, data=old_conf_1, unique_id=uid_1) + config_entry_1.add_to_hass(hass) + entity1 = entity_reg.async_get_or_create( + domain="sensor", + platform=DOMAIN, + unique_id=uid_1, + config_entry=config_entry_1, + suggested_object_id="test_pvpc_1", + ) + + config_entry_2 = MockConfigEntry(domain=DOMAIN, data=old_conf_2, unique_id=uid_2) + config_entry_2.add_to_hass(hass) + entity2 = entity_reg.async_get_or_create( + domain="sensor", + platform=DOMAIN, + unique_id=uid_2, + config_entry=config_entry_2, + suggested_object_id="test_pvpc_2", + ) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert len(entity_reg.entities) == 2 + + mock_data = {"return_time": datetime(2019, 10, 27, 20, tzinfo=date_util.UTC)} + + def mock_now(): + return mock_data["return_time"] + + caplog.clear() + with caplog.at_level(logging.WARNING): + with patch("homeassistant.util.dt.utcnow", new=mock_now): + assert await hass.config_entries.async_setup(config_entry_1.entry_id) + assert len(caplog.messages) == 2 + + # check migration with removal of extra sensors + assert len(entity_reg.entities) == 1 + assert entity1.entity_id in entity_reg.entities + assert entity2.entity_id not in entity_reg.entities + + current_entries = hass.config_entries.async_entries(DOMAIN) + assert len(current_entries) == 1 + migrated_entry = current_entries[0] + assert migrated_entry.version == 1 + assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] + assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] + + await hass.async_block_till_done() + assert pvpc_aioclient_mock.call_count == 2 diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json new file mode 100644 index 00000000000..59559d3c3f7 --- /dev/null +++ b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2021_06_01.json @@ -0,0 +1,604 @@ +{ + "PVPC": [ + { + "Dia": "01/06/2021", + "Hora": "00-01", + "PCB": "116,33", + "CYM": "116,33", + "COF2TD": "0,000088075182000000", + "PMHPCB": "104,00", + "PMHCYM": "104,00", + "SAHPCB": "3,56", + "SAHCYM": "3,56", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "01-02", + "PCB": "115,95", + "CYM": "115,95", + "COF2TD": "0,000073094842000000", + "PMHPCB": "103,18", + "PMHCYM": "103,18", + "SAHPCB": "3,99", + "SAHCYM": "3,99", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,58", + "CCVCYM": "2,58", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "02-03", + "PCB": "114,89", + "CYM": "114,89", + "COF2TD": "0,000065114032000000", + "PMHPCB": "101,87", + "PMHCYM": "101,87", + "SAHPCB": "4,25", + "SAHCYM": "4,25", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,56", + "CCVCYM": "2,56", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "03-04", + "PCB": "114,96", + "CYM": "114,96", + "COF2TD": "0,000061272596000000", + "PMHPCB": "102,01", + "PMHCYM": "102,01", + "SAHPCB": "4,19", + "SAHCYM": "4,19", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "04-05", + "PCB": "114,84", + "CYM": "114,84", + "COF2TD": "0,000059563056000000", + "PMHPCB": "101,87", + "PMHCYM": "101,87", + "SAHPCB": "4,21", + "SAHCYM": "4,21", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,56", + "CCVCYM": "2,56", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "05-06", + "PCB": "116,03", + "CYM": "116,03", + "COF2TD": "0,000059907686000000", + "PMHPCB": "103,14", + "PMHCYM": "103,14", + "SAHPCB": "4,11", + "SAHCYM": "4,11", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,58", + "CCVCYM": "2,58", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "06-07", + "PCB": "116,29", + "CYM": "116,29", + "COF2TD": "0,000062818713000000", + "PMHPCB": "103,64", + "PMHCYM": "103,64", + "SAHPCB": "3,88", + "SAHCYM": "3,88", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "07-08", + "PCB": "115,70", + "CYM": "115,70", + "COF2TD": "0,000072575564000000", + "PMHPCB": "103,85", + "PMHCYM": "103,85", + "SAHPCB": "3,10", + "SAHCYM": "3,10", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "6,00", + "TEUCYM": "6,00", + "CCVPCB": "2,55", + "CCVCYM": "2,55", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "08-09", + "PCB": "152,89", + "CYM": "152,89", + "COF2TD": "0,000086825264000000", + "PMHPCB": "105,65", + "PMHCYM": "105,65", + "SAHPCB": "2,36", + "SAHCYM": "2,36", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "0,34", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "09-10", + "PCB": "150,83", + "CYM": "150,83", + "COF2TD": "0,000095768317000000", + "PMHPCB": "103,77", + "PMHCYM": "103,77", + "SAHPCB": "2,24", + "SAHCYM": "2,24", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "0,34", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,53", + "CCVCYM": "2,53", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "10-11", + "PCB": "242,62", + "CYM": "149,28", + "COF2TD": "0,000102672431000000", + "PMHPCB": "102,38", + "PMHCYM": "102,11", + "SAHPCB": "2,38", + "SAHCYM": "2,37", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,01", + "PCAPCYM": "0,34", + "TEUPCB": "133,12", + "TEUCYM": "41,77", + "CCVPCB": "2,54", + "CCVCYM": "2,51", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "11-12", + "PCB": "240,50", + "CYM": "240,50", + "COF2TD": "0,000105691470000000", + "PMHPCB": "100,14", + "PMHCYM": "100,14", + "SAHPCB": "2,52", + "SAHCYM": "2,52", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,02", + "PCAPCYM": "2,02", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,51", + "CCVCYM": "2,51", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "12-13", + "PCB": "238,09", + "CYM": "238,09", + "COF2TD": "0,000110462952000000", + "PMHPCB": "97,58", + "PMHCYM": "97,58", + "SAHPCB": "2,71", + "SAHCYM": "2,71", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,02", + "PCAPCYM": "2,02", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,47", + "CCVCYM": "2,47", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "13-14", + "PCB": "235,30", + "CYM": "235,30", + "COF2TD": "0,000119052052000000", + "PMHPCB": "94,65", + "PMHCYM": "94,65", + "SAHPCB": "2,89", + "SAHCYM": "2,89", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,02", + "PCAPCYM": "2,02", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,43", + "CCVCYM": "2,43", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "14-15", + "PCB": "137,96", + "CYM": "231,28", + "COF2TD": "0,000117990009000000", + "PMHPCB": "89,95", + "PMHCYM": "90,19", + "SAHPCB": "3,37", + "SAHCYM": "3,38", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "2,03", + "TEUPCB": "41,77", + "TEUCYM": "133,12", + "CCVPCB": "2,34", + "CCVCYM": "2,37", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "15-16", + "PCB": "132,88", + "CYM": "132,88", + "COF2TD": "0,000108598330000000", + "PMHPCB": "84,43", + "PMHCYM": "84,43", + "SAHPCB": "3,89", + "SAHCYM": "3,89", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "0,34", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,26", + "CCVCYM": "2,26", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "16-17", + "PCB": "131,93", + "CYM": "131,93", + "COF2TD": "0,000104114191000000", + "PMHPCB": "83,66", + "PMHCYM": "83,66", + "SAHPCB": "3,73", + "SAHCYM": "3,73", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "0,34", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,25", + "CCVCYM": "2,25", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "17-18", + "PCB": "135,99", + "CYM": "135,99", + "COF2TD": "0,000105171071000000", + "PMHPCB": "88,07", + "PMHCYM": "88,07", + "SAHPCB": "3,31", + "SAHCYM": "3,31", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,34", + "PCAPCYM": "0,34", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,31", + "CCVCYM": "2,31", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "18-19", + "PCB": "231,44", + "CYM": "138,13", + "COF2TD": "0,000106417649000000", + "PMHPCB": "90,57", + "PMHCYM": "90,33", + "SAHPCB": "3,16", + "SAHCYM": "3,15", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,16", + "FOSCYM": "0,16", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,02", + "PCAPCYM": "0,34", + "TEUPCB": "133,12", + "TEUCYM": "41,77", + "CCVPCB": "2,37", + "CCVCYM": "2,34", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "19-20", + "PCB": "240,40", + "CYM": "240,40", + "COF2TD": "0,000108017615000000", + "PMHPCB": "99,53", + "PMHCYM": "99,53", + "SAHPCB": "3,00", + "SAHCYM": "3,00", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,04", + "PCAPCYM": "2,04", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,52", + "CCVCYM": "2,52", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "20-21", + "PCB": "246,20", + "CYM": "246,20", + "COF2TD": "0,000114631042000000", + "PMHPCB": "104,32", + "PMHCYM": "104,32", + "SAHPCB": "3,90", + "SAHCYM": "3,90", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,05", + "PCAPCYM": "2,05", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,61", + "CCVCYM": "2,61", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "21-22", + "PCB": "248,08", + "CYM": "248,08", + "COF2TD": "0,000127585671000000", + "PMHPCB": "107,28", + "PMHCYM": "107,28", + "SAHPCB": "2,78", + "SAHCYM": "2,78", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "2,06", + "PCAPCYM": "2,06", + "TEUPCB": "133,12", + "TEUCYM": "133,12", + "CCVPCB": "2,64", + "CCVCYM": "2,64", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "22-23", + "PCB": "155,91", + "CYM": "249,41", + "COF2TD": "0,000130129026000000", + "PMHPCB": "108,02", + "PMHCYM": "108,39", + "SAHPCB": "2,93", + "SAHCYM": "2,94", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,35", + "PCAPCYM": "2,09", + "TEUPCB": "41,77", + "TEUCYM": "133,12", + "CCVPCB": "2,64", + "CCVCYM": "2,67", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + }, + { + "Dia": "01/06/2021", + "Hora": "23-24", + "PCB": "156,50", + "CYM": "156,50", + "COF2TD": "0,000110367990000000", + "PMHPCB": "108,02", + "PMHCYM": "108,02", + "SAHPCB": "3,50", + "SAHCYM": "3,50", + "FOMPCB": "0,03", + "FOMCYM": "0,03", + "FOSPCB": "0,17", + "FOSCYM": "0,17", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,35", + "PCAPCYM": "0,35", + "TEUPCB": "41,77", + "TEUCYM": "41,77", + "CCVPCB": "2,66", + "CCVCYM": "2,66", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00" + } + ] +} \ No newline at end of file From 7947946793743cf3fef05f0c2a25aaf75bbde209 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 10:10:26 +0200 Subject: [PATCH 402/750] Type entry setup/unload for entity components (#51912) --- homeassistant/components/binary_sensor/__init__.py | 12 ++++++++---- homeassistant/components/calendar/__init__.py | 12 ++++++++---- homeassistant/components/climate/__init__.py | 10 ++++++---- homeassistant/components/cover/__init__.py | 12 ++++++++---- homeassistant/components/fan/__init__.py | 12 ++++++++---- homeassistant/components/geo_location/__init__.py | 12 ++++++++---- homeassistant/components/humidifier/__init__.py | 6 ++++-- homeassistant/components/lock/__init__.py | 12 ++++++++---- homeassistant/components/media_player/__init__.py | 12 ++++++++---- homeassistant/components/number/__init__.py | 6 ++++-- homeassistant/components/scene/__init__.py | 13 ++++++++----- homeassistant/components/switch/__init__.py | 8 +++++--- homeassistant/components/vacuum/__init__.py | 12 ++++++++---- homeassistant/components/water_heater/__init__.py | 12 ++++++++---- homeassistant/components/weather/__init__.py | 12 ++++++++---- 15 files changed, 107 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d698a9c306e..c9ff4254e40 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -7,7 +7,9 @@ from typing import final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -137,14 +139,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class BinarySensorEntity(Entity): diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 11a6916ba83..8809e05d25b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -9,7 +9,9 @@ from typing import cast, final from aiohttp import web from homeassistant.components import http +from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BAD_REQUEST, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -46,14 +48,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) def get_date(date): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index cd559a2e345..dbd74d1c5e8 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -157,14 +157,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistant, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class ClimateEntity(Entity): diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 60fd5a2af2c..110dd09098e 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -8,6 +8,7 @@ from typing import Any, final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, @@ -24,6 +25,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -156,14 +158,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class CoverEntity(Entity): diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f484ca36b25..20a11fd89f1 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -9,12 +9,14 @@ from typing import final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -204,14 +206,16 @@ async def async_setup(hass, config: dict): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) def _fan_native(method): diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 11294e73f63..c32917cb5cd 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -5,7 +5,9 @@ from datetime import timedelta import logging from typing import final +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -36,14 +38,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class GeolocationEvent(Entity): diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 31bff2fe3f4..7839eeec799 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -91,12 +91,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class HumidifierEntity(ToggleEntity): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 237daedae80..c74289b8794 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -6,6 +6,7 @@ from typing import final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, @@ -15,6 +16,7 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -66,14 +68,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class LockEntity(Entity): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0b4a5157c72..ffdabe6fed7 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -27,6 +27,7 @@ from homeassistant.components.websocket_api.const import ( ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND, @@ -52,6 +53,7 @@ from homeassistant.const import ( STATE_OFF, STATE_PLAYING, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -357,14 +359,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class MediaPlayerEntity(Entity): diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 897186dce7b..4b1049e36a2 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -56,12 +56,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class NumberEntity(Entity): diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index ced56fe5905..16e09f2cf39 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -9,8 +9,9 @@ from typing import Any import voluptuous as vol from homeassistant.components.light import ATTR_TRANSITION +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON -from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -75,14 +76,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class Scene(Entity): diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index db103915fa4..1ef48fed620 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, cast, final +from typing import Any, final import voluptuous as vol @@ -74,12 +74,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return cast(bool, await hass.data[DOMAIN].async_setup_entry(entry)) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return cast(bool, await hass.data[DOMAIN].async_unload_entry(entry)) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class SwitchEntity(ToggleEntity): diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index d8803931f38..36cc632d932 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -6,6 +6,7 @@ from typing import final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ATTR_BATTERY_LEVEL, ATTR_COMMAND, @@ -16,6 +17,7 @@ from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API STATE_ON, STATE_PAUSED, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -122,14 +124,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class _BaseVacuum(Entity): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 5ae22c77b5e..202a3dd057c 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -6,6 +6,7 @@ from typing import final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -18,6 +19,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -119,14 +121,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class WaterHeaterEntity(Entity): diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index da66c354d5a..a60011c205f 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -3,7 +3,9 @@ from datetime import timedelta import logging from typing import final +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -67,14 +69,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) class WeatherEntity(Entity): From 3b00e87ebcdc3264f21845c43a6034fdad5f4e68 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 10:16:16 +0200 Subject: [PATCH 403/750] Define WeatherEntity entity attributes as class variables (#51899) --- .../components/accuweather/weather.py | 6 +- homeassistant/components/smhi/weather.py | 7 +- homeassistant/components/weather/__init__.py | 101 ++++++++++++------ 3 files changed, 74 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 0dc4c7e270c..ee0ef69d666 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -156,12 +157,12 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): return None @property - def forecast(self) -> list[dict[str, Any]] | None: + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" if not self.coordinator.forecast: return None # remap keys from library to keys understood by the weather component - forecast = [ + return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], @@ -183,7 +184,6 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): } for item in self.coordinator.data[ATTR_FORECAST] ] - return forecast @staticmethod def _calc_precipitation(day: dict[str, Any]) -> float: diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d28cb51870b..ec99f2a12ae 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Final, TypedDict +from typing import Final, TypedDict import aiohttp import async_timeout @@ -31,6 +31,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -235,12 +236,12 @@ class SmhiWeather(WeatherEntity): return "Swedish weather institute (SMHI)" @property - def forecast(self) -> list[dict[str, Any]] | None: + def forecast(self) -> list[Forecast] | None: """Return the forecast.""" if self._forecasts is None or len(self._forecasts) < 2: return None - data = [] + data: list[Forecast] = [] for forecast in self._forecasts[1:]: condition = next( diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index a60011c205f..b737129e69e 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,7 +1,9 @@ """Weather component that handles meteorological data for your location.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import final +from typing import Final, TypedDict, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS @@ -35,15 +37,15 @@ ATTR_CONDITION_SUNNY = "sunny" ATTR_CONDITION_WINDY = "windy" ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_FORECAST = "forecast" -ATTR_FORECAST_CONDITION = "condition" -ATTR_FORECAST_PRECIPITATION = "precipitation" -ATTR_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_TEMP = "temperature" -ATTR_FORECAST_TEMP_LOW = "templow" -ATTR_FORECAST_TIME = "datetime" -ATTR_FORECAST_WIND_BEARING = "wind_bearing" -ATTR_FORECAST_WIND_SPEED = "wind_speed" +ATTR_FORECAST_CONDITION: Final = "condition" +ATTR_FORECAST_PRECIPITATION: Final = "precipitation" +ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability" +ATTR_FORECAST_PRESSURE: Final = "pressure" +ATTR_FORECAST_TEMP: Final = "temperature" +ATTR_FORECAST_TEMP_LOW: Final = "templow" +ATTR_FORECAST_TIME: Final = "datetime" +ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing" +ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" ATTR_WEATHER_ATTRIBUTION = "attribution" ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" @@ -60,6 +62,20 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=30) +class Forecast(TypedDict, total=False): + """Typed weather forecast dict.""" + + condition: str | None + datetime: str + precipitation_probability: int | None + precipitation: float | None + pressure: float | None + temperature: float | None + templow: float | None + wind_bearing: float | str | None + wind_speed: float | None + + async def async_setup(hass, config): """Set up the weather component.""" component = hass.data[DOMAIN] = EntityComponent( @@ -84,59 +100,75 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class WeatherEntity(Entity): """ABC for weather data.""" + _attr_attribution: str | None = None + _attr_condition: str | None + _attr_forecast: list[Forecast] | None = None + _attr_humidity: float | None = None + _attr_ozone: float | None = None + _attr_precision: float + _attr_pressure: float | None = None + _attr_state: None = None + _attr_temperature_unit: str + _attr_temperature: float | None + _attr_visibility: float | None = None + _attr_wind_bearing: float | str | None = None + _attr_wind_speed: float | None = None + @property - def temperature(self): + def temperature(self) -> float | None: """Return the platform temperature.""" - raise NotImplementedError() + return self._attr_temperature @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - raise NotImplementedError() + return self._attr_temperature_unit @property - def pressure(self): + def pressure(self) -> float | None: """Return the pressure.""" - return None + return self._attr_pressure @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" - raise NotImplementedError() + return self._attr_humidity @property - def wind_speed(self): + def wind_speed(self) -> float | None: """Return the wind speed.""" - return None + return self._attr_wind_speed @property - def wind_bearing(self): + def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - return None + return self._attr_wind_bearing @property - def ozone(self): + def ozone(self) -> float | None: """Return the ozone level.""" - return None + return self._attr_ozone @property - def attribution(self): + def attribution(self) -> str | None: """Return the attribution.""" - return None + return self._attr_attribution @property - def visibility(self): + def visibility(self) -> float | None: """Return the visibility.""" - return None + return self._attr_visibility @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast.""" - return None + return self._attr_forecast @property - def precision(self): + def precision(self) -> float: """Return the precision of the temperature value.""" + if hasattr(self, "_attr_precision"): + return self._attr_precision return ( PRECISION_TENTHS if self.temperature_unit == TEMP_CELSIUS @@ -205,11 +237,12 @@ class WeatherEntity(Entity): return data @property - def state(self): + @final + def state(self) -> str | None: """Return the current state.""" return self.condition @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" - raise NotImplementedError() + return self._attr_condition From 9f17b8856a12ffc18f43918b752411ccbfd17b84 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 10:19:29 +0200 Subject: [PATCH 404/750] Define WaterHeaterEntity entity attributes as class variables (#51903) --- homeassistant/components/demo/water_heater.py | 74 +++++-------------- .../components/water_heater/__init__.py | 64 ++++++++++------ 2 files changed, 61 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index 0b96bbf75f8..2311f0f457b 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -30,23 +30,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoWaterHeater(WaterHeaterEntity): """Representation of a demo water_heater device.""" + _attr_should_poll = False + _attr_supported_features = SUPPORT_FLAGS_HEATER + def __init__( self, name, target_temperature, unit_of_measurement, away, current_operation ): """Initialize the water_heater device.""" - self._name = name - self._support_flags = SUPPORT_FLAGS_HEATER + self._attr_name = name if target_temperature is not None: - self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE + self._attr_supported_features = ( + self.supported_features | SUPPORT_TARGET_TEMPERATURE + ) if away is not None: - self._support_flags = self._support_flags | SUPPORT_AWAY_MODE + self._attr_supported_features = self.supported_features | SUPPORT_AWAY_MODE if current_operation is not None: - self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE - self._target_temperature = target_temperature - self._unit_of_measurement = unit_of_measurement - self._away = away - self._current_operation = current_operation - self._operation_list = [ + self._attr_supported_features = ( + self.supported_features | SUPPORT_OPERATION_MODE + ) + self._attr_target_temperature = target_temperature + self._attr_temperature_unit = unit_of_measurement + self._attr_is_away_mode_on = away + self._attr_current_operation = current_operation + self._attr_operation_list = [ "eco", "electric", "performance", @@ -56,62 +62,22 @@ class DemoWaterHeater(WaterHeaterEntity): "off", ] - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def name(self): - """Return the name of the water_heater device.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return self._operation_list - - @property - def is_away_mode_on(self): - """Return if away mode is on.""" - return self._away - def set_temperature(self, **kwargs): """Set new target temperatures.""" - self._target_temperature = kwargs.get(ATTR_TEMPERATURE) + self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) self.schedule_update_ha_state() def set_operation_mode(self, operation_mode): """Set new operation mode.""" - self._current_operation = operation_mode + self._attr_current_operation = operation_mode self.schedule_update_ha_state() def turn_away_mode_on(self): """Turn away mode on.""" - self._away = True + self._attr_is_away_mode_on = True self.schedule_update_ha_state() def turn_away_mode_off(self): """Turn away mode off.""" - self._away = False + self._attr_is_away_mode_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 202a3dd057c..fcee7a446e0 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,4 +1,6 @@ """Support for water heater devices.""" +from __future__ import annotations + from datetime import timedelta import functools as ft import logging @@ -136,14 +138,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class WaterHeaterEntity(Entity): """Base class for water heater entities.""" + _attr_current_operation: str | None = None + _attr_current_temperature: float | None = None + _attr_is_away_mode_on: bool | None = None + _attr_max_temp: float + _attr_min_temp: float + _attr_operation_list: list[str] | None = None + _attr_precision: float + _attr_state: None = None + _attr_supported_features: int + _attr_target_temperature_high: float | None = None + _attr_target_temperature_low: float | None = None + _attr_target_temperature: float | None = None + _attr_temperature_unit: str + + @final @property - def state(self): + def state(self) -> str | None: """Return the current state.""" return self.current_operation @property - def precision(self): + def precision(self) -> float: """Return the precision of the system.""" + if hasattr(self, "_attr_precision"): + return self._attr_precision if self.hass.config.units.temperature_unit == TEMP_CELSIUS: return PRECISION_TENTHS return PRECISION_WHOLE @@ -210,44 +229,44 @@ class WaterHeaterEntity(Entity): return data @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" - raise NotImplementedError + return self._attr_temperature_unit @property - def current_operation(self): + def current_operation(self) -> str | None: """Return current operation ie. eco, electric, performance, ...""" - return None + return self._attr_current_operation @property - def operation_list(self): + def operation_list(self) -> list[str] | None: """Return the list of available operation modes.""" - return None + return self._attr_operation_list @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" - return None + return self._attr_current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return None + return self._attr_target_temperature @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" - return None + return self._attr_target_temperature_high @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" - return None + return self._attr_target_temperature_low @property - def is_away_mode_on(self): + def is_away_mode_on(self) -> bool | None: """Return true if away mode is on.""" - return None + return self._attr_is_away_mode_on def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -283,14 +302,11 @@ class WaterHeaterEntity(Entity): """Turn away mode off.""" await self.hass.async_add_executor_job(self.turn_away_mode_off) - @property - def supported_features(self): - """Return the list of supported features.""" - raise NotImplementedError() - @property def min_temp(self): """Return the minimum temperature.""" + if hasattr(self, "_attr_min_temp"): + return self._attr_min_temp return convert_temperature( DEFAULT_MIN_TEMP, TEMP_FAHRENHEIT, self.temperature_unit ) @@ -298,6 +314,8 @@ class WaterHeaterEntity(Entity): @property def max_temp(self): """Return the maximum temperature.""" + if hasattr(self, "_attr_max_temp"): + return self._attr_max_temp return convert_temperature( DEFAULT_MAX_TEMP, TEMP_FAHRENHEIT, self.temperature_unit ) From 17a71020db3527fe185b1ccc75bc253dd68343e0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 10:21:49 +0200 Subject: [PATCH 405/750] Define RemoteEntity entity attributes as class variables (#51904) --- homeassistant/components/remote/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index fef0da4dae6..0fc4255615e 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -145,20 +145,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class RemoteEntity(ToggleEntity): """Base class for remote entities.""" + _attr_activity_list: list[str] | None = None + _attr_current_activity: str | None = None + _attr_supported_features: int = 0 + @property def supported_features(self) -> int: """Flag supported features.""" - return 0 + return self._attr_supported_features @property def current_activity(self) -> str | None: """Active activity.""" - return None + return self._attr_current_activity @property def activity_list(self) -> list[str] | None: """List of available activities.""" - return None + return self._attr_activity_list @final @property From ee6c77048c5bd270eb9eade7e1b048fe02df252a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Jun 2021 10:27:22 +0200 Subject: [PATCH 406/750] Improve editing of device actions referencing non-added humidifier (#51749) --- .../components/humidifier/device_action.py | 22 +- .../humidifier/test_device_action.py | 345 ++++++++++-------- 2 files changed, 203 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 7e11b65fd2e..81df6938236 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -7,15 +7,15 @@ from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, - ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_capability, get_supported_features from . import DOMAIN, const @@ -50,7 +50,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) + supported_features = get_supported_features(hass, entry.entity_id) base_action = { CONF_DEVICE_ID: device_id, @@ -59,11 +59,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: } actions.append({**base_action, CONF_TYPE: "set_humidity"}) - # We need a state or else we can't populate the available modes. - if state is None: - continue - - if state.attributes[ATTR_SUPPORTED_FEATURES] & const.SUPPORT_MODES: + if supported_features & const.SUPPORT_MODES: actions.append({**base_action, CONF_TYPE: "set_mode"}) return actions @@ -93,7 +89,6 @@ async def async_call_action_from_config( async def async_get_action_capabilities(hass, config): """List action capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) action_type = config[CONF_TYPE] fields = {} @@ -101,9 +96,12 @@ async def async_get_action_capabilities(hass, config): if action_type == "set_humidity": fields[vol.Required(const.ATTR_HUMIDITY)] = vol.Coerce(int) elif action_type == "set_mode": - if state: - available_modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, []) - else: + try: + available_modes = ( + get_capability(hass, config[ATTR_ENTITY_ID], const.ATTR_AVAILABLE_MODES) + or [] + ) + except HomeAssistantError: available_modes = [] fields[vol.Required(ATTR_MODE)] = vol.In(available_modes) else: diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 1bf1c110ec6..39767b569ac 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -31,7 +31,24 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg): +@pytest.mark.parametrize( + "set_state,features_reg,features_state,expected_action_types", + [ + (False, 0, 0, []), + (False, const.SUPPORT_MODES, 0, ["set_mode"]), + (True, 0, 0, []), + (True, 0, const.SUPPORT_MODES, ["set_mode"]), + ], +) +async def test_get_actions( + hass, + device_reg, + entity_reg, + set_state, + features_reg, + features_state, + expected_action_types, +): """Test we get the expected actions from a humidifier.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -39,124 +56,36 @@ async def test_get_actions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set("humidifier.test_5678", STATE_ON, {}) - hass.states.async_set( - "humidifier.test_5678", "attributes", {"supported_features": 1} + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=features_reg, ) - expected_actions = [ + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", "attributes", {"supported_features": features_state} + ) + expected_actions = [] + basic_action_types = ["turn_on", "turn_off", "toggle", "set_humidity"] + expected_actions += [ { "domain": DOMAIN, - "type": "turn_on", + "type": action, "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "set_humidity", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "set_mode", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in basic_action_types ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_action_no_modes(hass, device_reg, entity_reg): - """Test we get the expected actions from a humidifier.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - hass.states.async_set("humidifier.test_5678", STATE_ON, {}) - hass.states.async_set( - "humidifier.test_5678", "attributes", {"supported_features": 0} - ) - expected_actions = [ + expected_actions += [ { "domain": DOMAIN, - "type": "turn_on", + "type": action, "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "set_humidity", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - ] - actions = await async_get_device_automations(hass, "action", device_entry.id) - assert_lists_same(actions, expected_actions) - - -async def test_get_action_no_state(hass, device_reg, entity_reg): - """Test we get the expected actions from a humidifier.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, - { - "domain": DOMAIN, - "type": "set_humidity", - "device_id": device_entry.id, - "entity_id": "humidifier.test_5678", - }, + "entity_id": f"{DOMAIN}.test_5678", + } + for action in expected_action_types ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert_lists_same(actions, expected_actions) @@ -291,69 +220,181 @@ async def test_action(hass): assert len(toggle_calls) == 1 -async def test_capabilities(hass): +@pytest.mark.parametrize( + "set_state,capabilities_reg,capabilities_state,action,expected_capabilities", + [ + ( + False, + {}, + {}, + "set_humidity", + [ + { + "name": "humidity", + "required": True, + "type": "integer", + } + ], + ), + ( + False, + {}, + {}, + "set_mode", + [ + { + "name": "mode", + "options": [], + "required": True, + "type": "select", + } + ], + ), + ( + False, + {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]}, + {}, + "set_mode", + [ + { + "name": "mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {}, + "set_humidity", + [ + { + "name": "humidity", + "required": True, + "type": "integer", + } + ], + ), + ( + True, + {}, + {}, + "set_mode", + [ + { + "name": "mode", + "options": [], + "required": True, + "type": "select", + } + ], + ), + ( + True, + {}, + {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]}, + "set_mode", + [ + { + "name": "mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ], + ), + ], +) +async def test_capabilities( + hass, + device_reg, + entity_reg, + set_state, + capabilities_reg, + capabilities_state, + action, + expected_capabilities, +): """Test getting capabilities.""" - # Test capabililities without state + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + capabilities=capabilities_reg, + ) + if set_state: + hass.states.async_set( + f"{DOMAIN}.test_5678", + STATE_ON, + capabilities_state, + ) + capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "humidifier.entity", - "type": "set_mode", + "entity_id": f"{DOMAIN}.test_5678", + "type": action, }, ) assert capabilities and "extra_fields" in capabilities - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "mode", "options": [], "required": True, "type": "select"}] - - # Set state - hass.states.async_set( - "humidifier.entity", - STATE_ON, - {const.ATTR_AVAILABLE_MODES: [const.MODE_HOME, const.MODE_AWAY]}, + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities ) - # Set humidity + +@pytest.mark.parametrize( + "action,capability_name,extra", + [ + ("set_humidity", "humidity", {"type": "integer"}), + ("set_mode", "mode", {"type": "select", "options": []}), + ], +) +async def test_capabilities_missing_entity( + hass, device_reg, entity_reg, action, capability_name, extra +): + """Test getting capabilities.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": "humidifier.entity", - "type": "set_humidity", + "entity_id": f"{DOMAIN}.test_5678", + "type": action, }, ) - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "humidity", "required": True, "type": "integer"}] - - # Set mode - capabilities = await device_action.async_get_action_capabilities( - hass, + expected_capabilities = [ { - "domain": DOMAIN, - "device_id": "abcdefgh", - "entity_id": "humidifier.entity", - "type": "set_mode", - }, - ) - - assert capabilities and "extra_fields" in capabilities - - assert voluptuous_serialize.convert( - capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [ - { - "name": "mode", - "options": [("home", "home"), ("away", "away")], + "name": capability_name, "required": True, - "type": "select", + **extra, } ] + + assert capabilities and "extra_fields" in capabilities + + assert ( + voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) + == expected_capabilities + ) From 3ba90776c0d72f7c2d3646f8d9979c85a5ed3337 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 17 Jun 2021 10:57:20 +0200 Subject: [PATCH 407/750] Add autospec to modbus mock, in order to use getattr (#51813) --- tests/components/modbus/conftest.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 7f67a0653fb..43221b219e5 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -97,12 +97,9 @@ async def base_test( mock_sync = mock.MagicMock() with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_sync - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusSerialClient", + "homeassistant.components.modbus.modbus.ModbusTcpClient", + autospec=True, return_value=mock_sync, - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusUdpClient", return_value=mock_sync ): # Setup inputs for the sensor From b2aa55cea2e216a7c3566300507e07298bc9dcf1 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 17 Jun 2021 10:58:28 +0200 Subject: [PATCH 408/750] Bump pydaikin to 2.4.3 (#51926) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index ec0b2716053..704cfcf739c 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.2"], + "requirements": ["pydaikin==2.4.3"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 3ba957f69c3..136dad05b50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1363,7 +1363,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.2 +pydaikin==2.4.3 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e82fa369708..aa4d61cc095 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -758,7 +758,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.2 +pydaikin==2.4.3 # homeassistant.components.deconz pydeconz==79 From 016ba39dfb2528b3a2d1975c3d9411ec15ae037e Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Thu, 17 Jun 2021 03:59:13 -0500 Subject: [PATCH 409/750] Ecobee logging cleanup (#51754) --- .../components/ecobee/binary_sensor.py | 14 ++---- homeassistant/components/ecobee/climate.py | 44 +++++++++++-------- homeassistant/components/ecobee/humidifier.py | 2 +- homeassistant/components/ecobee/sensor.py | 12 ++--- homeassistant/components/ecobee/weather.py | 12 +---- tests/components/ecobee/test_climate.py | 4 +- 6 files changed, 38 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 9593fc0e497..0d3174cb5a6 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) -from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER async def async_setup_entry(hass, config_entry, async_add_entities): @@ -67,17 +67,11 @@ class EcobeeBinarySensor(BinarySensorEntity): f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" ) except KeyError: - _LOGGER.error( - "Model number for ecobee thermostat %s not recognized. " - "Please visit this link and provide the following information: " - "https://github.com/home-assistant/core/issues/27172 " - "Unrecognized model number: %s", - thermostat["name"], - thermostat["modelNumber"], - ) + # Ecobee model is not in our list + model = None break - if identifier is not None and model is not None: + if identifier is not None: return { "identifiers": {(DOMAIN, identifier)}, "name": self.sensor_name, diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 9e9e2eff1c8..eeac7ddb224 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -176,10 +176,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ecobee thermostat.""" data = hass.data[DOMAIN] + entities = [] - devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))] + for index in range(len(data.ecobee.thermostats)): + thermostat = data.ecobee.get_thermostat(index) + if not thermostat["modelNumber"] in ECOBEE_MODEL_TO_NAME: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link to open a new issue: " + "https://github.com/home-assistant/core/issues " + "and include the following information: " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + entities.append(Thermostat(data, index, thermostat)) - async_add_entities(devices, True) + async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() @@ -187,7 +200,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Create a vacation on the target thermostat.""" entity_id = service.data[ATTR_ENTITY_ID] - for thermostat in devices: + for thermostat in entities: if thermostat.entity_id == entity_id: thermostat.create_vacation(service.data) thermostat.schedule_update_ha_state(True) @@ -198,7 +211,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_id = service.data[ATTR_ENTITY_ID] vacation_name = service.data[ATTR_VACATION_NAME] - for thermostat in devices: + for thermostat in entities: if thermostat.entity_id == entity_id: thermostat.delete_vacation(vacation_name) thermostat.schedule_update_ha_state(True) @@ -211,10 +224,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entity_id: target_thermostats = [ - device for device in devices if device.entity_id in entity_id + entity for entity in entities if entity.entity_id in entity_id ] else: - target_thermostats = devices + target_thermostats = entities for thermostat in target_thermostats: thermostat.set_fan_min_on_time(str(fan_min_on_time)) @@ -228,10 +241,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entity_id: target_thermostats = [ - device for device in devices if device.entity_id in entity_id + entity for entity in entities if entity.entity_id in entity_id ] else: - target_thermostats = devices + target_thermostats = entities for thermostat in target_thermostats: thermostat.resume_program(resume_all) @@ -291,11 +304,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class Thermostat(ClimateEntity): """A thermostat class for Ecobee.""" - def __init__(self, data, thermostat_index): + def __init__(self, data, thermostat_index, thermostat): """Initialize the thermostat.""" self.data = data self.thermostat_index = thermostat_index - self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + self.thermostat = thermostat self._name = self.thermostat["name"] self.vacation = None self._last_active_hvac_mode = HVAC_MODE_HEAT_COOL @@ -358,15 +371,8 @@ class Thermostat(ClimateEntity): try: model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" except KeyError: - _LOGGER.error( - "Model number for ecobee thermostat %s not recognized. " - "Please visit this link and provide the following information: " - "https://github.com/home-assistant/core/issues/27172 " - "Unrecognized model number: %s", - self.name, - self.thermostat["modelNumber"], - ) - return None + # Ecobee model is not in our list + model = None return { "identifiers": {(DOMAIN, self.thermostat["identifier"])}, diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index f39fb7acc68..fadaa83155a 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -60,7 +60,7 @@ class EcobeeHumidifier(HumidifierEntity): model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" except KeyError: # Ecobee model is not in our list - return None + model = None return { "identifiers": {(DOMAIN, self.thermostat["identifier"])}, diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 5abe809e59d..7cf04e12718 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) -from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -79,14 +79,8 @@ class EcobeeSensor(SensorEntity): f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" ) except KeyError: - _LOGGER.error( - "Model number for ecobee thermostat %s not recognized. " - "Please visit this link and provide the following information: " - "https://github.com/home-assistant/core/issues/27172 " - "Unrecognized model number: %s", - thermostat["name"], - thermostat["modelNumber"], - ) + # Ecobee model is not in our list + model = None break if identifier is not None and model is not None: diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index fc93ebffb95..8e3de2be90a 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -17,7 +17,6 @@ from homeassistant.util import dt as dt_util from homeassistant.util.pressure import convert as pressure_convert from .const import ( - _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, ECOBEE_WEATHER_SYMBOL_TO_HASS, @@ -72,15 +71,8 @@ class EcobeeWeather(WeatherEntity): try: model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" except KeyError: - _LOGGER.error( - "Model number for ecobee thermostat %s not recognized. " - "Please visit this link and provide the following information: " - "https://github.com/home-assistant/core/issues/27172 " - "Unrecognized model number: %s", - thermostat["name"], - thermostat["modelNumber"], - ) - return None + # Ecobee model is not in our list + model = None return { "identifiers": {(DOMAIN, thermostat["identifier"])}, diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index da6017a71a1..cc394a1f63f 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -13,6 +13,7 @@ def ecobee_fixture(): """Set up ecobee mock.""" vals = { "name": "Ecobee", + "modelNumber": "athenaSmart", "program": { "climates": [ {"name": "Climate1", "climateRef": "c1"}, @@ -64,7 +65,8 @@ def data_fixture(ecobee_fixture): @pytest.fixture(name="thermostat") def thermostat_fixture(data): """Set up ecobee thermostat object.""" - return ecobee.Thermostat(data, 1) + thermostat = data.ecobee.get_thermostat(1) + return ecobee.Thermostat(data, 1, thermostat) async def test_name(thermostat): From d3724355cf67f5df3ee9553eda001a88952ba03f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Jun 2021 04:09:57 -0500 Subject: [PATCH 410/750] Improve Sonos Spotify/Tidal support, add service exceptions (#51871) --- homeassistant/components/sonos/helpers.py | 18 +++++--- .../components/sonos/media_player.py | 43 ++++++++----------- homeassistant/components/sonos/speaker.py | 11 +++++ 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 6f22d8ab417..ac8cd00d9db 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -7,11 +7,13 @@ from typing import Any, Callable from pysonos.exceptions import SoCoException, SoCoUPnPException +from homeassistant.exceptions import HomeAssistantError + _LOGGER = logging.getLogger(__name__) def soco_error(errorcodes: list[str] | None = None) -> Callable: - """Filter out specified UPnP errors from logs and avoid exceptions.""" + """Filter out specified UPnP errors and raise exceptions for service calls.""" def decorator(funct: Callable) -> Callable: """Decorate functions.""" @@ -21,11 +23,15 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: """Wrap for all soco UPnP exception.""" try: return funct(*args, **kwargs) - except SoCoUPnPException as err: - if not errorcodes or err.error_code not in errorcodes: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - except SoCoException as err: - _LOGGER.error("Error on %s with %s", funct.__name__, err) + except (OSError, SoCoException, SoCoUPnPException) as err: + error_code = getattr(err, "error_code", None) + function = funct.__name__ + if errorcodes and error_code in errorcodes: + _LOGGER.debug( + "Error code %s ignored in call to %s", error_code, function + ) + return + raise HomeAssistantError(f"Error calling {function}: {err}") from err return wrapper diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 8b1664d4f6c..a4cc6e175ec 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -13,8 +13,6 @@ from pysonos.core import ( PLAY_MODE_BY_MEANING, PLAY_MODES, ) -from pysonos.exceptions import SoCoUPnPException -from pysonos.plugins.sharelink import ShareLinkPlugin import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -518,6 +516,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_id is a Plex payload, attempt Plex->Sonos playback. + If media_id is a Sonos or Tidal share link, attempt playback + using the respective service. + If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. @@ -527,28 +528,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if media_id and media_id.startswith(PLEX_URI_SCHEME): media_id = media_id[len(PLEX_URI_SCHEME) :] play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call] - elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): - share_link = ShareLinkPlugin(soco) + return + + share_link = self.speaker.share_link + if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE): - try: - if share_link.is_share_link(media_id): - share_link.add_share_link_to_queue(media_id) - else: - soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error( - 'Error parsing media uri "%s", ' - "please check it's a valid media resource " - "supported by Sonos", - media_id, - ) + share_link.add_share_link_to_queue(media_id) else: - if share_link.is_share_link(media_id): - soco.clear_queue() - share_link.add_share_link_to_queue(media_id) - soco.play_from_queue(0) - else: - soco.play_uri(media_id) + soco.clear_queue() + share_link.add_share_link_to_queue(media_id) + soco.play_from_queue(0) + elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + if kwargs.get(ATTR_MEDIA_ENQUEUE): + soco.add_uri_to_queue(media_id) + else: + soco.play_uri(media_id) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] @@ -557,11 +551,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): try: playlists = soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) + except StopIteration: + _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) + else: soco.clear_queue() soco.add_to_queue(playlist) soco.play_from_queue(0) - except StopIteration: - _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) elif media_type in PLAYABLE_MEDIA_TYPES: item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 88e6ac33ba7..b010fa623be 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -16,6 +16,7 @@ from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException from pysonos.music_library import MusicLibrary +from pysonos.plugins.sharelink import ShareLinkPlugin from pysonos.snapshot import Snapshot from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -147,6 +148,7 @@ class SonosSpeaker: self.soco = soco self.household_id: str = soco.household_id self.media = SonosMedia(soco) + self._share_link_plugin: ShareLinkPlugin | None = None # Synchronization helpers self._is_ready: bool = False @@ -292,6 +294,13 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None + @property + def share_link(self) -> ShareLinkPlugin: + """Cache the ShareLinkPlugin instance for this speaker.""" + if not self._share_link_plugin: + self._share_link_plugin = ShareLinkPlugin(self.soco) + return self._share_link_plugin + @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -476,6 +485,8 @@ class SonosSpeaker: self, now: datetime.datetime | None = None, will_reconnect: bool = False ) -> None: """Make this player unavailable when it was not seen recently.""" + self._share_link_plugin = None + if self._seen_timer: self._seen_timer() self._seen_timer = None From 927b5361a362cc5ae6d7f1713a609646a62a9283 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 11:25:33 +0200 Subject: [PATCH 411/750] Define LockEntity entity attributes as class variables (#51909) --- homeassistant/components/demo/lock.py | 38 ++++++----------------- homeassistant/components/lock/__init__.py | 22 ++++++++----- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 63f2d218957..cafc0e3f748 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -22,44 +22,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoLock(LockEntity): """Representation of a Demo lock.""" - def __init__(self, name, state, openable=False): + _attr_should_poll = False + + def __init__(self, name: str, state: str, openable: bool = False) -> None: """Initialize the lock.""" - self._name = name - self._state = state - self._openable = openable - - @property - def should_poll(self): - """No polling needed for a demo lock.""" - return False - - @property - def name(self): - """Return the name of the lock if any.""" - return self._name - - @property - def is_locked(self): - """Return true if lock is locked.""" - return self._state == STATE_LOCKED + self._attr_name = name + self._attr_is_locked = state == STATE_LOCKED + if openable: + self._attr_supported_features = SUPPORT_OPEN def lock(self, **kwargs): """Lock the device.""" - self._state = STATE_LOCKED + self._attr_is_locked = True self.schedule_update_ha_state() def unlock(self, **kwargs): """Unlock the device.""" - self._state = STATE_UNLOCKED + self._attr_is_locked = False self.schedule_update_ha_state() def open(self, **kwargs): """Open the door latch.""" - self._state = STATE_UNLOCKED + self._attr_is_locked = False self.schedule_update_ha_state() - - @property - def supported_features(self): - """Flag supported features.""" - if self._openable: - return SUPPORT_OPEN diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index c74289b8794..1ca7404d56a 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -1,4 +1,6 @@ """Component to interface with locks that can be controlled remotely.""" +from __future__ import annotations + from datetime import timedelta import functools as ft import logging @@ -83,20 +85,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class LockEntity(Entity): """Base class for lock entities.""" + _attr_changed_by: str | None = None + _attr_code_format: str | None = None + _attr_is_locked: bool | None = None + _attr_state: None = None + @property - def changed_by(self): + def changed_by(self) -> str | None: """Last change triggered by.""" - return None + return self._attr_changed_by @property - def code_format(self): + def code_format(self) -> str | None: """Regex for code format or None if no code is required.""" - return None + return self._attr_code_format @property - def is_locked(self): + def is_locked(self) -> bool | None: """Return true if the lock is locked.""" - return None + return self._attr_is_locked def lock(self, **kwargs): """Lock the lock.""" @@ -133,8 +140,9 @@ class LockEntity(Entity): state_attr[attr] = value return state_attr + @final @property - def state(self): + def state(self) -> str | None: """Return the state.""" locked = self.is_locked if locked is None: From 0327d0b6db4724a1229c209ba61d4da498d94d7a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Jun 2021 11:40:48 +0200 Subject: [PATCH 412/750] Add Mutesync dynamic update interval and catch invalid response values (#50764) Co-authored-by: Paulus Schoutsen --- homeassistant/components/mutesync/__init__.py | 17 +++++++++++++---- homeassistant/components/mutesync/const.py | 7 ++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index 7bee5ff5a9b..af14725a3b4 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -1,7 +1,6 @@ """The mütesync integration.""" from __future__ import annotations -from datetime import timedelta import logging import async_timeout @@ -11,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator -from .const import DOMAIN +from .const import DOMAIN, UPDATE_INTERVAL_IN_MEETING, UPDATE_INTERVAL_NOT_IN_MEETING PLATFORMS = ["binary_sensor"] @@ -27,7 +26,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_data(): """Update the data.""" async with async_timeout.timeout(2.5): - return await client.get_state() + state = await client.get_state() + + if state["muted"] is None or state["in_meeting"] is None: + raise update_coordinator.UpdateFailed("Got invalid response") + + if state["in_meeting"]: + coordinator.update_interval = UPDATE_INTERVAL_IN_MEETING + else: + coordinator.update_interval = UPDATE_INTERVAL_NOT_IN_MEETING + + return state coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id @@ -35,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, logging.getLogger(__name__), name=DOMAIN, - update_interval=timedelta(seconds=5), + update_interval=UPDATE_INTERVAL_NOT_IN_MEETING, update_method=update_data, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py index fcf05584f42..5e288b405af 100644 --- a/homeassistant/components/mutesync/const.py +++ b/homeassistant/components/mutesync/const.py @@ -1,3 +1,8 @@ """Constants for the mütesync integration.""" +from datetime import timedelta +from typing import Final -DOMAIN = "mutesync" +DOMAIN: Final = "mutesync" + +UPDATE_INTERVAL_NOT_IN_MEETING: Final = timedelta(seconds=10) +UPDATE_INTERVAL_IN_MEETING: Final = timedelta(seconds=5) From 08b0ef7a5e4c586efd268cb5e06d913d909f6647 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 17 Jun 2021 12:27:05 +0200 Subject: [PATCH 413/750] Use test fixture for configuration testing (#51803) * Autospec mock_modbus and usei for configuration. * Review comment. --- tests/components/modbus/conftest.py | 12 +- tests/components/modbus/test_binary_sensor.py | 43 +++--- tests/components/modbus/test_climate.py | 47 +++--- tests/components/modbus/test_cover.py | 45 +++--- tests/components/modbus/test_fan.py | 127 +++++++++------- tests/components/modbus/test_light.py | 127 +++++++++------- tests/components/modbus/test_sensor.py | 116 +++++++++------ tests/components/modbus/test_switch.py | 139 ++++++++++-------- 8 files changed, 363 insertions(+), 293 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 43221b219e5..2902f0f7675 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -40,7 +40,7 @@ def mock_pymodbus(): @pytest.fixture -async def mock_modbus(hass, mock_pymodbus): +async def mock_modbus(hass, do_config): """Load integration modbus using mocked pymodbus.""" config = { DOMAIN: [ @@ -49,12 +49,16 @@ async def mock_modbus(hass, mock_pymodbus): CONF_HOST: "modbusTestHost", CONF_PORT: 5501, CONF_NAME: TEST_MODBUS_NAME, + **do_config, } ] } - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - yield mock_pymodbus + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True + ) as mock_pb: + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + yield mock_pb # dataclass diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index ebf487b129d..17f5dfb87a4 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -25,33 +25,32 @@ from tests.common import mock_restore_cache @pytest.mark.parametrize( - "do_options", + "do_config", [ - {}, { - CONF_SLAVE: 10, - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - CONF_DEVICE_CLASS: "door", + CONF_BINARY_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + } + ] + }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_DEVICE_CLASS: "door", + } + ] }, ], ) -async def test_config_binary_sensor(hass, do_options): - """Run test for binary sensor.""" - sensor_name = "test_sensor" - config_sensor = { - CONF_NAME: sensor_name, - CONF_ADDRESS: 51, - **do_options, - } - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_BINARY_SENSORS, - None, - method_discovery=True, - ) +async def test_config_binary_sensor(hass, mock_modbus): + """Run config test for binary sensor.""" + assert SENSOR_DOMAIN in hass.config.components @pytest.mark.parametrize("do_type", [CALL_TYPE_COIL, CALL_TYPE_DISCRETE]) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index c5201593480..0f02470d10a 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -20,34 +20,35 @@ from tests.common import mock_restore_cache @pytest.mark.parametrize( - "do_options", + "do_config", [ - {}, { - CONF_SCAN_INTERVAL: 20, - CONF_COUNT: 2, + CONF_CLIMATES: [ + { + CONF_NAME: "test_climate", + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + } + ], + }, + { + CONF_CLIMATES: [ + { + CONF_NAME: "test_climate", + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 20, + CONF_COUNT: 2, + } + ], }, ], ) -async def test_config_climate(hass, do_options): - """Run test for climate.""" - device_name = "test_climate" - device_config = { - CONF_NAME: device_name, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - **do_options, - } - await base_config_test( - hass, - device_config, - device_name, - CLIMATE_DOMAIN, - CONF_CLIMATES, - None, - method_discovery=True, - ) +async def test_config_climate(hass, mock_modbus): + """Run configuration test for climate.""" + assert CLIMATE_DOMAIN in hass.config.components @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 98c4e84699f..e2f58173442 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -35,34 +35,33 @@ from tests.common import mock_restore_cache @pytest.mark.parametrize( - "do_options", + "do_config", [ - {}, { - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 20, + CONF_COVERS: [ + { + CONF_NAME: "test_cover", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + }, + { + CONF_COVERS: [ + { + CONF_NAME: "test_cover", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 20, + } + ] }, ], ) -@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) -async def test_config_cover(hass, do_options, read_type): - """Run test for cover.""" - device_name = "test_cover" - device_config = { - CONF_NAME: device_name, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: read_type, - **do_options, - } - await base_config_test( - hass, - device_config, - device_name, - COVER_DOMAIN, - CONF_COVERS, - None, - method_discovery=True, - ) +async def test_config_cover(hass, mock_modbus): + """Run configuration test for cover.""" + assert COVER_DOMAIN in hass.config.components @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 2a9414d2277..1f24bf7231c 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -41,75 +41,90 @@ from tests.common import mock_restore_cache "do_config", [ { - CONF_ADDRESS: 1234, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: None, + CONF_FANS: [ + { + CONF_NAME: "test_fan", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + } + ] }, ], ) -async def test_config_fan(hass, do_config): - """Run test for fan.""" - device_name = "test_fan" - - device_config = { - CONF_NAME: device_name, - **do_config, - } - - await base_config_test( - hass, - device_config, - device_name, - FAN_DOMAIN, - CONF_FANS, - None, - method_discovery=True, - ) +async def test_config_fan(hass, mock_modbus): + """Run configuration test for fan.""" + assert FAN_DOMAIN in hass.config.components @pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 12e72e54155..a7826681678 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -41,75 +41,90 @@ from tests.common import mock_restore_cache "do_config", [ { - CONF_ADDRESS: 1234, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY: None, + CONF_LIGHTS: [ + { + CONF_NAME: "test_light", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + } + ] }, ], ) -async def test_config_light(hass, do_config): - """Run test for light.""" - device_name = "test_light" - - device_config = { - CONF_NAME: device_name, - **do_config, - } - - await base_config_test( - hass, - device_config, - device_name, - LIGHT_DOMAIN, - CONF_LIGHTS, - None, - method_discovery=True, - ) +async def test_config_light(hass, mock_modbus): + """Run configuration test for light.""" + assert LIGHT_DOMAIN in hass.config.components @pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4deb5ee8392..e581fd8fb1d 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -45,68 +45,90 @@ from tests.common import mock_restore_cache "do_config", [ { - CONF_ADDRESS: 51, + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + } + ] }, { - CONF_ADDRESS: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DEVICE_CLASS: "battery", + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_COUNT: 1, + CONF_DATA_TYPE: "int", + CONF_PRECISION: 0, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DEVICE_CLASS: "battery", + } + ] }, { - CONF_ADDRESS: 51, - CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", - CONF_PRECISION: 0, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DEVICE_CLASS: "battery", + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_COUNT: 1, + CONF_DATA_TYPE: "int", + CONF_PRECISION: 0, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_DEVICE_CLASS: "battery", + } + ] }, { - CONF_ADDRESS: 51, - CONF_COUNT: 1, - CONF_SWAP: CONF_SWAP_NONE, + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_NONE, + } + ] }, { - CONF_ADDRESS: 51, - CONF_COUNT: 1, - CONF_SWAP: CONF_SWAP_BYTE, + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_COUNT: 1, + CONF_SWAP: CONF_SWAP_BYTE, + } + ] }, { - CONF_ADDRESS: 51, - CONF_COUNT: 2, - CONF_SWAP: CONF_SWAP_WORD, + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD, + } + ] }, { - CONF_ADDRESS: 51, - CONF_COUNT: 2, - CONF_SWAP: CONF_SWAP_WORD_BYTE, + CONF_SENSORS: [ + { + CONF_NAME: "test_sensor", + CONF_ADDRESS: 51, + CONF_COUNT: 2, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + } + ] }, ], ) -async def test_config_sensor(hass, do_config): - """Run test for sensor.""" - sensor_name = "test_sensor" - config_sensor = { - CONF_NAME: sensor_name, - **do_config, - } - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_SENSORS, - CONF_REGISTERS, - method_discovery=True, - ) +async def test_config_sensor(hass, mock_modbus): + """Run configuration test for sensor.""" + assert SENSOR_DOMAIN in hass.config.components @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 37ddfec2b4d..eb4efbc048c 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -48,81 +48,96 @@ from tests.common import async_fire_time_changed, mock_restore_cache "do_config", [ { - CONF_ADDRESS: 1234, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - CONF_DELAY: 10, - }, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + CONF_DELAY: 10, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_VERIFY: { - CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, - CONF_ADDRESS: 1235, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - }, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + } + ] }, { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_SCAN_INTERVAL: 0, - CONF_VERIFY: None, + CONF_SWITCHES: [ + { + CONF_NAME: "test_switch", + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_SCAN_INTERVAL: 0, + CONF_VERIFY: None, + } + ] }, ], ) -async def test_config_switch(hass, do_config): - """Run test for switch.""" - device_name = "test_switch" - - device_config = { - CONF_NAME: device_name, - **do_config, - } - - await base_config_test( - hass, - device_config, - device_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - None, - method_discovery=True, - ) +async def test_config_switch(hass, mock_modbus): + """Run configurationtest for switch.""" + assert SWITCH_DOMAIN in hass.config.components @pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) From db61a773fdcf2cbb06382467d91f7dc80073d752 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 17 Jun 2021 13:33:44 +0300 Subject: [PATCH 414/750] Add remote control platform to BraviaTV (#50845) --- .coveragerc | 1 + CODEOWNERS | 2 +- homeassistant/components/braviatv/__init__.py | 246 ++++++++++++++++- .../components/braviatv/config_flow.py | 7 +- homeassistant/components/braviatv/const.py | 2 +- .../components/braviatv/manifest.json | 2 +- .../components/braviatv/media_player.py | 252 +++++------------- homeassistant/components/braviatv/remote.py | 69 +++++ 8 files changed, 384 insertions(+), 197 deletions(-) create mode 100644 homeassistant/components/braviatv/remote.py diff --git a/.coveragerc b/.coveragerc index 4b0a1f8c4a3..0c2da893245 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,6 +122,7 @@ omit = homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py + homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py homeassistant/components/broadlink/const.py homeassistant/components/broadlink/remote.py diff --git a/CODEOWNERS b/CODEOWNERS index 158b375163f..3f895d6572d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,7 +72,7 @@ homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bond/* @prystupa homeassistant/components/bosch_shc/* @tschamm -homeassistant/components/braviatv/* @bieniu +homeassistant/components/braviatv/* @bieniu @Drafteed homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 0097964e298..17e02f2c8f0 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,24 +1,47 @@ """The Bravia TV component.""" +import asyncio +from datetime import timedelta +import logging from bravia_tv import BraviaRC +from bravia_tv.braviarc import NoIPControl -from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import BRAVIARC, DOMAIN, UNDO_UPDATE_LISTENER +from .const import ( + BRAVIA_COORDINATOR, + CLIENTID_PREFIX, + CONF_IGNORED_SOURCES, + DOMAIN, + NICKNAME, + UNDO_UPDATE_LISTENER, +) -PLATFORMS = ["media_player"] +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry(hass, config_entry): """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] + pin = config_entry.data[CONF_PIN] + ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) + coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources) undo_listener = config_entry.add_update_listener(update_listener) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { - BRAVIARC: BraviaRC(host, mac), + BRAVIA_COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, } @@ -44,3 +67,218 @@ async def async_unload_entry(hass, config_entry): async def update_listener(hass, config_entry): """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) + + +class BraviaTVCoordinator(DataUpdateCoordinator[None]): + """Representation of a Bravia TV Coordinator. + + An instance is used per device to share the same power state between + several platforms. + """ + + def __init__(self, hass, host, mac, pin, ignored_sources): + """Initialize Bravia TV Client.""" + + self.braviarc = BraviaRC(host, mac) + self.pin = pin + self.ignored_sources = ignored_sources + self.muted = False + self.program_name = None + self.channel_name = None + self.channel_number = None + self.source = None + self.source_list = [] + self.original_content_list = [] + self.content_mapping = {} + self.duration = None + self.content_uri = None + self.start_date_time = None + self.program_media_type = None + self.audio_output = None + self.min_volume = None + self.max_volume = None + self.volume = None + self.is_on = False + # Assume that the TV is in Play mode + self.playing = True + self.state_lock = asyncio.Lock() + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=1.0, immediate=False + ), + ) + + def _send_command(self, command, repeats=1): + """Send a command to the TV.""" + for _ in range(repeats): + for cmd in command: + self.braviarc.send_command(cmd) + + def _get_source(self): + """Return the name of the source.""" + for key, value in self.content_mapping.items(): + if value == self.content_uri: + return key + + def _refresh_volume(self): + """Refresh volume information.""" + volume_info = self.braviarc.get_volume_info(self.audio_output) + if volume_info is not None: + self.audio_output = volume_info.get("target") + self.volume = volume_info.get("volume") + self.min_volume = volume_info.get("minVolume") + self.max_volume = volume_info.get("maxVolume") + self.muted = volume_info.get("mute") + return True + return False + + def _refresh_channels(self): + """Refresh source and channels list.""" + if not self.source_list: + self.content_mapping = self.braviarc.load_source_list() + self.source_list = [] + if not self.content_mapping: + return False + for key in self.content_mapping: + if key not in self.ignored_sources: + self.source_list.append(key) + return True + + def _refresh_playing_info(self): + """Refresh playing information.""" + playing_info = self.braviarc.get_playing_info() + self.program_name = playing_info.get("programTitle") + self.channel_name = playing_info.get("title") + self.program_media_type = playing_info.get("programMediaType") + self.channel_number = playing_info.get("dispNum") + self.content_uri = playing_info.get("uri") + self.source = self._get_source() + self.duration = playing_info.get("durationSec") + self.start_date_time = playing_info.get("startDateTime") + if not playing_info: + self.channel_name = "App" + + def _update_tv_data(self): + """Connect and update TV info.""" + power_status = self.braviarc.get_power_status() + + if power_status != "off": + connected = self.braviarc.is_connected() + if not connected: + try: + connected = self.braviarc.connect( + self.pin, CLIENTID_PREFIX, NICKNAME + ) + except NoIPControl: + _LOGGER.error("IP Control is disabled in the TV settings") + if not connected: + power_status = "off" + + if power_status == "active": + self.is_on = True + if self._refresh_volume() and self._refresh_channels(): + self._refresh_playing_info() + return + + self.is_on = False + + async def _async_update_data(self): + """Fetch the latest data.""" + if self.state_lock.locked(): + return + + await self.hass.async_add_executor_job(self._update_tv_data) + + async def async_turn_on(self): + """Turn the device on.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.turn_on) + await self.async_request_refresh() + + async def async_turn_off(self): + """Turn off device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.turn_off) + await self.async_request_refresh() + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + async with self.state_lock: + await self.hass.async_add_executor_job( + self.braviarc.set_volume_level, volume, self.audio_output + ) + await self.async_request_refresh() + + async def async_volume_up(self): + """Send volume up command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job( + self.braviarc.volume_up, self.audio_output + ) + await self.async_request_refresh() + + async def async_volume_down(self): + """Send volume down command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job( + self.braviarc.volume_down, self.audio_output + ) + await self.async_request_refresh() + + async def async_volume_mute(self, mute): + """Send mute command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute) + await self.async_request_refresh() + + async def async_media_play(self): + """Send play command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.media_play) + self.playing = True + await self.async_request_refresh() + + async def async_media_pause(self): + """Send pause command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.media_pause) + self.playing = False + await self.async_request_refresh() + + async def async_media_stop(self): + """Send stop command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.media_stop) + self.playing = False + await self.async_request_refresh() + + async def async_media_next_track(self): + """Send next track command.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.media_next_track) + await self.async_request_refresh() + + async def async_media_previous_track(self): + """Send previous track command.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.media_previous_track) + await self.async_request_refresh() + + async def async_select_source(self, source): + """Set the input source.""" + if source in self.content_mapping: + uri = self.content_mapping[source] + async with self.state_lock: + await self.hass.async_add_executor_job(self.braviarc.play_content, uri) + await self.async_request_refresh() + + async def async_send_command(self, command, repeats): + """Send command to device.""" + async with self.state_lock: + await self.hass.async_add_executor_job(self._send_command, command, repeats) + await self.async_request_refresh() diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 02856887d17..e042c705b98 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -16,7 +16,7 @@ from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, - BRAVIARC, + BRAVIA_COORDINATOR, CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, @@ -160,7 +160,10 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" - self.braviarc = self.hass.data[DOMAIN][self.config_entry.entry_id][BRAVIARC] + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][ + BRAVIA_COORDINATOR + ] + self.braviarc = coordinator.braviarc connected = await self.hass.async_add_executor_job(self.braviarc.is_connected) if not connected: await self.hass.async_add_executor_job( diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index a5d7a88d4c3..bc06b7c858a 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -6,7 +6,7 @@ ATTR_MODEL = "model" CONF_IGNORED_SOURCES = "ignored_sources" -BRAVIARC = "braviarc" +BRAVIA_COORDINATOR = "bravia_coordinator" BRAVIA_CONFIG_FILE = "bravia.conf" CLIENTID_PREFIX = "HomeAssistant" DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV" diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index f7456c08c13..18285ebec00 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": ["bravia-tv==1.0.11"], - "codeowners": ["@bieniu"], + "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling" } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 90ad562c0ed..bb4ad32ed9b 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,8 +1,6 @@ """Support for interface with a Bravia TV.""" -import asyncio import logging -from bravia_tv.braviarc import NoIPControl import voluptuous as vol from homeassistant.components.media_player import ( @@ -24,19 +22,24 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PIN, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.json import load_json from .const import ( ATTR_MANUFACTURER, BRAVIA_CONFIG_FILE, - BRAVIARC, - CLIENTID_PREFIX, - CONF_IGNORED_SOURCES, + BRAVIA_COORDINATOR, DEFAULT_NAME, DOMAIN, - NICKNAME, ) _LOGGER = logging.getLogger(__name__) @@ -94,9 +97,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Add BraviaTV entities from a config_entry.""" - ignored_sources = [] - pin = config_entry.data[CONF_PIN] + """Set up Bravia TV Media Player from a config_entry.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id][BRAVIA_COORDINATOR] unique_id = config_entry.unique_id device_info = { "identifiers": {(DOMAIN, unique_id)}, @@ -105,135 +108,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "model": config_entry.title, } - braviarc = hass.data[DOMAIN][config_entry.entry_id][BRAVIARC] - - ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) - async_add_entities( - [ - BraviaTVDevice( - braviarc, DEFAULT_NAME, pin, unique_id, device_info, ignored_sources - ) - ] + [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] ) -class BraviaTVDevice(MediaPlayerEntity): - """Representation of a Bravia TV.""" +class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): + """Representation of a Bravia TV Media Player.""" _attr_device_class = DEVICE_CLASS_TV + _attr_supported_features = SUPPORT_BRAVIA - def __init__(self, client, name, pin, unique_id, device_info, ignored_sources): - """Initialize the Bravia TV device.""" + def __init__(self, coordinator, name, unique_id, device_info): + """Initialize the entity.""" - self._pin = pin - self._braviarc = client self._name = name - self._state = STATE_OFF - self._muted = False - self._program_name = None - self._channel_name = None - self._channel_number = None - self._source = None - self._source_list = [] - self._original_content_list = [] - self._content_mapping = {} - self._duration = None - self._content_uri = None - self._playing = False - self._start_date_time = None - self._program_media_type = None - self._audio_output = None - self._min_volume = None - self._max_volume = None - self._volume = None self._unique_id = unique_id self._device_info = device_info - self._ignored_sources = ignored_sources - self._state_lock = asyncio.Lock() - async def async_update(self): - """Update TV info.""" - if self._state_lock.locked(): - return - - power_status = await self.hass.async_add_executor_job( - self._braviarc.get_power_status - ) - - if power_status != "off": - connected = await self.hass.async_add_executor_job( - self._braviarc.is_connected - ) - if not connected: - try: - connected = await self.hass.async_add_executor_job( - self._braviarc.connect, self._pin, CLIENTID_PREFIX, NICKNAME - ) - except NoIPControl: - _LOGGER.error("IP Control is disabled in the TV settings") - if not connected: - power_status = "off" - - if power_status == "active": - self._state = STATE_ON - if ( - await self._async_refresh_volume() - and await self._async_refresh_channels() - ): - await self._async_refresh_playing_info() - return - self._state = STATE_OFF - - def _get_source(self): - """Return the name of the source.""" - for key, value in self._content_mapping.items(): - if value == self._content_uri: - return key - - async def _async_refresh_volume(self): - """Refresh volume information.""" - volume_info = await self.hass.async_add_executor_job( - self._braviarc.get_volume_info, self._audio_output - ) - if volume_info is not None: - self._audio_output = volume_info.get("target") - self._volume = volume_info.get("volume") - self._min_volume = volume_info.get("minVolume") - self._max_volume = volume_info.get("maxVolume") - self._muted = volume_info.get("mute") - return True - return False - - async def _async_refresh_channels(self): - """Refresh source and channels list.""" - if not self._source_list: - self._content_mapping = await self.hass.async_add_executor_job( - self._braviarc.load_source_list - ) - self._source_list = [] - if not self._content_mapping: - return False - for key in self._content_mapping: - if key not in self._ignored_sources: - self._source_list.append(key) - return True - - async def _async_refresh_playing_info(self): - """Refresh Playing information.""" - playing_info = await self.hass.async_add_executor_job( - self._braviarc.get_playing_info - ) - self._program_name = playing_info.get("programTitle") - self._channel_name = playing_info.get("title") - self._program_media_type = playing_info.get("programMediaType") - self._channel_number = playing_info.get("dispNum") - self._content_uri = playing_info.get("uri") - self._source = self._get_source() - self._duration = playing_info.get("durationSec") - self._start_date_time = playing_info.get("startDateTime") - if not playing_info: - self._channel_name = "App" + super().__init__(coordinator) @property def name(self): @@ -253,113 +146,96 @@ class BraviaTVDevice(MediaPlayerEntity): @property def state(self): """Return the state of the device.""" - return self._state + if self.coordinator.is_on: + return STATE_PLAYING if self.coordinator.playing else STATE_PAUSED + return STATE_OFF @property def source(self): """Return the current input source.""" - return self._source + return self.coordinator.source @property def source_list(self): """List of available input sources.""" - return self._source_list + return self.coordinator.source_list @property def volume_level(self): """Volume level of the media player (0..1).""" - if self._volume is not None: - return self._volume / 100 + if self.coordinator.volume is not None: + return self.coordinator.volume / 100 return None @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - return self._muted - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_BRAVIA + return self.coordinator.muted @property def media_title(self): """Title of current playing media.""" return_value = None - if self._channel_name is not None: - return_value = self._channel_name - if self._program_name is not None: - return_value = f"{return_value}: {self._program_name}" + if self.coordinator.channel_name is not None: + return_value = self.coordinator.channel_name + if self.coordinator.program_name is not None: + return_value = f"{return_value}: {self.coordinator.program_name}" return return_value @property def media_content_id(self): """Content ID of current playing media.""" - return self._channel_name + return self.coordinator.channel_name @property def media_duration(self): """Duration of current playing media in seconds.""" - return self._duration - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._braviarc.set_volume_level(volume, self._audio_output) + return self.coordinator.duration async def async_turn_on(self): - """Turn the media player on.""" - async with self._state_lock: - await self.hass.async_add_executor_job(self._braviarc.turn_on) + """Turn the device on.""" + await self.coordinator.async_turn_on() async def async_turn_off(self): - """Turn off media player.""" - async with self._state_lock: - await self.hass.async_add_executor_job(self._braviarc.turn_off) + """Turn the device off.""" + await self.coordinator.async_turn_off() - def volume_up(self): - """Volume up the media player.""" - self._braviarc.volume_up(self._audio_output) + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self.coordinator.async_set_volume_level(volume) - def volume_down(self): - """Volume down media player.""" - self._braviarc.volume_down(self._audio_output) + async def async_volume_up(self): + """Send volume up command.""" + await self.coordinator.async_volume_up() - def mute_volume(self, mute): + async def async_volume_down(self): + """Send volume down command.""" + await self.coordinator.async_volume_down() + + async def async_mute_volume(self, mute): """Send mute command.""" - self._braviarc.mute_volume(mute) + await self.coordinator.async_volume_mute(mute) - def select_source(self, source): + async def async_select_source(self, source): """Set the input source.""" - if source in self._content_mapping: - uri = self._content_mapping[source] - self._braviarc.play_content(uri) + await self.coordinator.async_select_source(source) - def media_play_pause(self): - """Simulate play pause media player.""" - if self._playing: - self.media_pause() - else: - self.media_play() - - def media_play(self): + async def async_media_play(self): """Send play command.""" - self._playing = True - self._braviarc.media_play() + await self.coordinator.async_media_play() - def media_pause(self): - """Send media pause command to media player.""" - self._playing = False - self._braviarc.media_pause() + async def async_media_pause(self): + """Send pause command.""" + await self.coordinator.async_media_pause() - def media_stop(self): + async def async_media_stop(self): """Send media stop command to media player.""" - self._playing = False - self._braviarc.media_stop() + await self.coordinator.async_media_stop() - def media_next_track(self): + async def async_media_next_track(self): """Send next track command.""" - self._braviarc.media_next_track() + await self.coordinator.async_media_next_track() - def media_previous_track(self): - """Send the previous track command.""" - self._braviarc.media_previous_track() + async def async_media_previous_track(self): + """Send previous track command.""" + await self.coordinator.async_media_previous_track() diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py new file mode 100644 index 00000000000..ca36276a88c --- /dev/null +++ b/homeassistant/components/braviatv/remote.py @@ -0,0 +1,69 @@ +"""Remote control support for Bravia TV.""" + +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTR_MANUFACTURER, BRAVIA_COORDINATOR, DEFAULT_NAME, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Bravia TV Remote from a config entry.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id][BRAVIA_COORDINATOR] + unique_id = config_entry.unique_id + device_info = { + "identifiers": {(DOMAIN, unique_id)}, + "name": DEFAULT_NAME, + "manufacturer": ATTR_MANUFACTURER, + "model": config_entry.title, + } + + async_add_entities( + [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] + ) + + +class BraviaTVRemote(CoordinatorEntity, RemoteEntity): + """Representation of a Bravia TV Remote.""" + + def __init__(self, coordinator, name, unique_id, device_info): + """Initialize the entity.""" + + self._name = name + self._unique_id = unique_id + self._device_info = device_info + + super().__init__(coordinator) + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._unique_id + + @property + def device_info(self): + """Return device specific attributes.""" + return self._device_info + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self.coordinator.is_on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self.coordinator.async_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self.coordinator.async_turn_off() + + async def async_send_command(self, command, **kwargs): + """Send a command to device.""" + repeats = kwargs[ATTR_NUM_REPEATS] + await self.coordinator.async_send_command(command, repeats) From 8e07e607413260c1dbc8a19f7175220f54d27572 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 13:53:45 +0200 Subject: [PATCH 415/750] Fully type binary_sensor entity component (#51957) --- homeassistant/components/binary_sensor/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index c9ff4254e40..ff97e9af601 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import final +from typing import Any, final import voluptuous as vol @@ -16,9 +16,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import StateType - -# mypy: allow-untyped-defs, no-check-untyped-defs +from homeassistant.helpers.typing import ConfigType, StateType _LOGGER = logging.getLogger(__name__) @@ -129,7 +127,7 @@ DEVICE_CLASSES = [ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for binary sensors.""" component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL @@ -172,9 +170,9 @@ class BinarySensorEntity(Entity): class BinarySensorDevice(BinarySensorEntity): """Represent a binary sensor (for backwards compatibility).""" - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any): """Print deprecation warning.""" - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # type: ignore[call-arg] _LOGGER.warning( "BinarySensorDevice is deprecated, modify %s to extend BinarySensorEntity", cls.__name__, From 06c2e541c458813e9d0a9a256e78ac49c95d28ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Jun 2021 14:28:56 +0200 Subject: [PATCH 416/750] Fully type lock entity component (#51958) --- homeassistant/components/lock/__init__.py | 25 +++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 1ca7404d56a..9e8bf3a740c 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import functools as ft import logging -from typing import final +from typing import Any, final import voluptuous as vol @@ -27,8 +27,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - -# mypy: allow-untyped-defs, no-check-untyped-defs +from homeassistant.helpers.typing import ConfigType, StateType _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,7 @@ SUPPORT_OPEN = 1 PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT} -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for locks.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -105,33 +104,33 @@ class LockEntity(Entity): """Return true if the lock is locked.""" return self._attr_is_locked - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the lock.""" raise NotImplementedError() - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" await self.hass.async_add_executor_job(ft.partial(self.lock, **kwargs)) - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" raise NotImplementedError() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" await self.hass.async_add_executor_job(ft.partial(self.unlock, **kwargs)) - def open(self, **kwargs): + def open(self, **kwargs: Any) -> None: """Open the door latch.""" raise NotImplementedError() - async def async_open(self, **kwargs): + async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" await self.hass.async_add_executor_job(ft.partial(self.open, **kwargs)) @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, StateType]: """Return the state attributes.""" state_attr = {} for prop, attr in PROP_TO_ATTR.items(): @@ -153,9 +152,9 @@ class LockEntity(Entity): class LockDevice(LockEntity): """Representation of a lock (for backwards compatibility).""" - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls, **kwargs: Any): """Print deprecation warning.""" - super().__init_subclass__(**kwargs) + super().__init_subclass__(**kwargs) # type: ignore[call-arg] _LOGGER.warning( "LockDevice is deprecated, modify %s to extend LockEntity", cls.__name__, From 1e18011603e5b8b790ff58d4929fadef890ac314 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 17 Jun 2021 11:19:25 -0400 Subject: [PATCH 417/750] Adjust zwave_js WS API commands for logging (#51096) --- homeassistant/components/zwave_js/api.py | 43 ++++++++++++----- tests/components/zwave_js/test_api.py | 61 ++++++++++++++++-------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 754fdfc25ab..5cf98d44802 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -159,7 +159,7 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_heal_node) websocket_api.async_register_command(hass, websocket_set_config_parameter) websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_subscribe_logs) + websocket_api.async_register_command(hass, websocket_subscribe_log_updates) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) websocket_api.async_register_command( @@ -1022,13 +1022,13 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/subscribe_logs", + vol.Required(TYPE): "zwave_js/subscribe_log_updates", vol.Required(ENTRY_ID): str, } ) @websocket_api.async_response @async_get_entry -async def websocket_subscribe_logs( +async def websocket_subscribe_log_updates( hass: HomeAssistant, connection: ActiveConnection, msg: dict, @@ -1042,24 +1042,44 @@ async def websocket_subscribe_logs( def async_cleanup() -> None: """Remove signal listeners.""" hass.async_create_task(driver.async_stop_listening_logs()) - unsub() + for unsub in unsubs: + unsub() @callback - def forward_event(event: dict) -> None: + def log_messages(event: dict) -> None: log_msg: LogMessage = event["log_message"] connection.send_message( websocket_api.event_message( msg[ID], { - "timestamp": log_msg.timestamp, - "level": log_msg.level, - "primary_tags": log_msg.primary_tags, - "message": log_msg.formatted_message, + "type": "log_message", + "log_message": { + "timestamp": log_msg.timestamp, + "level": log_msg.level, + "primary_tags": log_msg.primary_tags, + "message": log_msg.formatted_message, + }, }, ) ) - unsub = driver.on("logging", forward_event) + @callback + def log_config_updates(event: dict) -> None: + log_config: LogConfig = event["log_config"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "type": "log_config", + "log_config": dataclasses.asdict(log_config), + }, + ) + ) + + unsubs = [ + driver.on("logging", log_messages), + driver.on("log config updated", log_config_updates), + ] connection.subscriptions[msg["id"]] = async_cleanup await driver.async_start_listening_logs() @@ -1126,10 +1146,9 @@ async def websocket_get_log_config( client: Client, ) -> None: """Get log configuration for the Z-Wave JS driver.""" - result = await client.driver.async_get_log_config() connection.send_result( msg[ID], - dataclasses.asdict(result), + dataclasses.asdict(client.driver.log_config), ) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f0b5a470df0..b6d846898d3 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1553,15 +1553,15 @@ async def test_view_invalid_node_id(integration, hass_client, method, url): assert resp.status == 404 -async def test_subscribe_logs(hass, integration, client, hass_ws_client): - """Test the subscribe_logs websocket command.""" +async def test_subscribe_log_updates(hass, integration, client, hass_ws_client): + """Test the subscribe_log_updates websocket command.""" entry = integration ws_client = await hass_ws_client(hass) client.async_send_command.return_value = {} await ws_client.send_json( - {ID: 1, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id} + {ID: 1, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -1588,10 +1588,41 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client): msg = await ws_client.receive_json() assert msg["event"] == { - "message": ["test"], - "level": "debug", - "primary_tags": "tag", - "timestamp": "time", + "type": "log_message", + "log_message": { + "message": ["test"], + "level": "debug", + "primary_tags": "tag", + "timestamp": "time", + }, + } + + event = Event( + type="log config updated", + data={ + "source": "driver", + "event": "log config updated", + "config": { + "enabled": False, + "level": "error", + "logToFile": True, + "filename": "test", + "forceConsole": True, + }, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "type": "log_config", + "log_config": { + "enabled": False, + "level": "error", + "log_to_file": True, + "filename": "test", + "force_console": True, + }, } # Test sending command with not loaded entry fails @@ -1599,7 +1630,7 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client): await hass.async_block_till_done() await ws_client.send_json( - {ID: 2, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id} + {ID: 2, TYPE: "zwave_js/subscribe_log_updates", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() @@ -1750,16 +1781,6 @@ async def test_get_log_config(hass, client, integration, hass_ws_client): ws_client = await hass_ws_client(hass) # Test we can get log configuration - client.async_send_command.return_value = { - "success": True, - "config": { - "enabled": True, - "level": "error", - "logToFile": False, - "filename": "/test.txt", - "forceConsole": False, - }, - } await ws_client.send_json( { ID: 1, @@ -1773,9 +1794,9 @@ async def test_get_log_config(hass, client, integration, hass_ws_client): log_config = msg["result"] assert log_config["enabled"] - assert log_config["level"] == LogLevel.ERROR + assert log_config["level"] == LogLevel.INFO assert log_config["log_to_file"] is False - assert log_config["filename"] == "/test.txt" + assert log_config["filename"] == "" assert log_config["force_console"] is False # Test sending command with not loaded entry fails From 88eca8cc15d754b598c5a96c5492e42cec9b2b1e Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 17 Jun 2021 17:57:27 +0200 Subject: [PATCH 418/750] Add deconz support for Lidl Smart Door Bell HG06668 (#51949) --- homeassistant/components/deconz/device_trigger.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 1784afa76a4..d7e42808851 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -450,6 +450,11 @@ GIRA_JUNG_SWITCH = { (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, } +LIDL_SILVERCREST_DOORBELL_MODEL = "HG06668" +LIDL_SILVERCREST_DOORBELL = { + (CONF_SHORT_PRESS, ""): {CONF_EVENT: 1002}, +} + LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY" LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY" LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY" @@ -564,6 +569,7 @@ REMOTES = { GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + LIDL_SILVERCREST_DOORBELL_MODEL: LIDL_SILVERCREST_DOORBELL, LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, From c0a0c8b28315961511d98f56776dfdbb9551b1e3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 18 Jun 2021 00:09:11 +0000 Subject: [PATCH 419/750] [ci skip] Translation update --- .../pvpc_hourly_pricing/translations/ca.json | 21 ++++++++++++++++--- .../pvpc_hourly_pricing/translations/et.json | 21 ++++++++++++++++--- .../pvpc_hourly_pricing/translations/nl.json | 15 +++++++++++++ .../translations/zh-Hant.json | 21 ++++++++++++++++--- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index bc5f3a59428..ef5e00e25c3 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Nom del sensor", - "tariff": "Tarifa contractada (1, 2 o 3 per\u00edodes)" + "power": "Pot\u00e8ncia contractada (kW)", + "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", + "tariff": "Tarifa aplicable per zona geogr\u00e0fica" }, - "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" + "description": "Aquest sensor utilitza l'API oficial 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/).", + "title": "Configuraci\u00f3 del sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Pot\u00e8ncia contractada (kW)", + "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", + "tariff": "Tarifa aplicable per zona geogr\u00e0fica" + }, + "description": "Aquest sensor utilitza l'API oficial 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/).", + "title": "Configuraci\u00f3 del sensor" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/et.json b/homeassistant/components/pvpc_hourly_pricing/translations/et.json index e80e5ef68cd..5db4f9ec04b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/et.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/et.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Anduri nimi", - "tariff": "Lepinguline tariif (1, 2 v\u00f5i 3 perioodi)" + "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", + "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", + "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" }, - "description": "See andur kasutab ametlikku API-d, et saada Hispaania [tunni hinnakujundus (PVPC)] (https://www.esios.ree.es/es/pvpc) elektri tunnihind.\n T\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \n\n Vali lepinguline m\u00e4\u00e4r l\u00e4htudes arveldusperioodide arvust p\u00e4evas:\n - 1 periood: normaalne\n - 2 perioodi: v\u00e4hendatud (\u00f6\u00f6hind)\n - 3 perioodi: elektriauto (\u00f6\u00f6 hind 3 perioodi)", - "title": "Tariifivalik" + "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Anduri seadistamine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", + "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", + "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" + }, + "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Anduri seadistamine" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index 5048ed498df..f74662b06da 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -7,11 +7,26 @@ "user": { "data": { "name": "Sensornaam", + "power": "Gecontracteerd vermogen (kW)", + "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", "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" } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Gecontracteerd vermogen (kW)", + "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", + "tariff": "Toepasselijk tarief per geografische zone" + }, + "description": "Deze sensor maakt gebruik van offici\u00eble API om [uurprijzen van elektriciteit (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanje te krijgen.\nGa voor een preciezere uitleg naar de [integratiedocumenten](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Sensor setup" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json index 15b6539281c..1cadebbc6be 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "\u50b3\u611f\u5668\u540d\u7a31", - "tariff": "\u5408\u7d04\u8cbb\u7387\uff081\u30012 \u6216 3 \u9031\u671f\uff09" + "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09", + "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09", + "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387" }, - "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002\n\n\u57fa\u65bc\u6bcf\u5929\u7684\u5e33\u55ae\u9031\u671f\u9078\u64c7\u5408\u7d04\u8cbb\u7387\uff1a\n- 1 \u9031\u671f\uff1a\u4e00\u822c\n- 2 \u9031\u671f\uff1a\u5dee\u5225\u8cbb\u7387\uff08\u591c\u9593\u8cbb\u7387\uff09\n- 3 \u9031\u671f\uff1a\u96fb\u52d5\u8eca\uff08\u591c\u9593\u8cbb\u7387 3 \u9031\u671f\uff09", - "title": "\u8cbb\u7387\u9078\u64c7" + "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", + "title": "\u611f\u61c9\u5668\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09", + "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09", + "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387" + }, + "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", + "title": "\u611f\u61c9\u5668\u8a2d\u5b9a" } } } From c149ecf2cc6a4efef00d537baed200b9f96bd4ed Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Fri, 18 Jun 2021 02:06:31 -0500 Subject: [PATCH 420/750] Handle disconnected ecobee thermostat in humidifier and remote sensors (#51873) --- homeassistant/components/ecobee/binary_sensor.py | 6 ++++++ homeassistant/components/ecobee/humidifier.py | 5 +++++ homeassistant/components/ecobee/sensor.py | 6 ++++++ tests/components/ecobee/test_climate.py | 1 + tests/fixtures/ecobee/ecobee-data.json | 2 +- 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 0d3174cb5a6..b81a5e6bef6 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -80,6 +80,12 @@ class EcobeeBinarySensor(BinarySensorEntity): } return None + @property + def available(self): + """Return true if device is available.""" + thermostat = self.data.ecobee.get_thermostat(self.index) + return thermostat["runtime"]["connected"] + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index fadaa83155a..984609c2f22 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -69,6 +69,11 @@ class EcobeeHumidifier(HumidifierEntity): "model": model, } + @property + def available(self): + """Return if device is available.""" + return self.thermostat["runtime"]["connected"] + async def async_update(self): """Get the latest state from the thermostat.""" if self.update_without_throttle: diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 7cf04e12718..275db46ab0a 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -92,6 +92,12 @@ class EcobeeSensor(SensorEntity): } return None + @property + def available(self): + """Return true if device is available.""" + thermostat = self.data.ecobee.get_thermostat(self.index) + return thermostat["runtime"]["connected"] + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index cc394a1f63f..ec466197995 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -22,6 +22,7 @@ def ecobee_fixture(): "currentClimateRef": "c1", }, "runtime": { + "connected": True, "actualTemperature": 300, "actualHumidity": 15, "desiredHeat": 400, diff --git a/tests/fixtures/ecobee/ecobee-data.json b/tests/fixtures/ecobee/ecobee-data.json index 6e679616085..a4caa72798d 100644 --- a/tests/fixtures/ecobee/ecobee-data.json +++ b/tests/fixtures/ecobee/ecobee-data.json @@ -12,7 +12,7 @@ "currentClimateRef": "c1" }, "runtime": { - "connected": false, + "connected": true, "actualTemperature": 300, "actualHumidity": 15, "desiredHeat": 400, From bc329cb60218b27e7edc62c5cafa4df04eaefa64 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Jun 2021 11:20:44 +0200 Subject: [PATCH 421/750] Convert if/elif chains to dicts in modbus (#51962) --- homeassistant/components/modbus/__init__.py | 8 +- homeassistant/components/modbus/const.py | 4 + homeassistant/components/modbus/modbus.py | 192 +++++++++----------- 3 files changed, 92 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8c37ea04079..4c4a1390887 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -69,7 +69,9 @@ from .const import ( CONF_RETRIES, CONF_RETRY_ON_EMPTY, CONF_REVERSE_ORDER, + CONF_RTUOVERTCP, CONF_SCALE, + CONF_SERIAL, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -86,6 +88,8 @@ from .const import ( CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, + CONF_TCP, + CONF_UDP, CONF_VERIFY, CONF_WRITE_TYPE, DATA_TYPE_CUSTOM, @@ -292,7 +296,7 @@ MODBUS_SCHEMA = vol.Schema( SERIAL_SCHEMA = MODBUS_SCHEMA.extend( { - vol.Required(CONF_TYPE): "serial", + vol.Required(CONF_TYPE): CONF_SERIAL, vol.Required(CONF_BAUDRATE): cv.positive_int, vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8), vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"), @@ -306,7 +310,7 @@ ETHERNET_SCHEMA = MODBUS_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"), + vol.Required(CONF_TYPE): vol.Any(CONF_TCP, CONF_UDP, CONF_RTUOVERTCP), } ) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 7074431b0a9..8fb4626d2fe 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -39,7 +39,9 @@ CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" +CONF_RTUOVERTCP = "rtuovertcp" CONF_SCALE = "scale" +CONF_SERIAL = "serial" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OFF = "state_off" @@ -56,6 +58,8 @@ CONF_SWAP_NONE = "none" CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" CONF_TARGET_TEMP = "target_temp_register" +CONF_TCP = "tcp" +CONF_UDP = "udp" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 495a22f8180..35572baff43 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -5,7 +5,6 @@ import logging from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException -from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( CONF_DELAY, @@ -41,7 +40,11 @@ from .const import ( CONF_PARITY, CONF_RETRIES, CONF_RETRY_ON_EMPTY, + CONF_RTUOVERTCP, + CONF_SERIAL, CONF_STOPBITS, + CONF_TCP, + CONF_UDP, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, PLATFORMS, @@ -51,9 +54,53 @@ from .const import ( ENTRY_FUNC = "func" ENTRY_ATTR = "attr" +ENTRY_NAME = "name" _LOGGER = logging.getLogger(__name__) +PYMODBUS_CALL = { + CALL_TYPE_COIL: { + ENTRY_ATTR: "bits", + ENTRY_NAME: "read_coils", + ENTRY_FUNC: None, + }, + CALL_TYPE_DISCRETE: { + ENTRY_ATTR: "bits", + ENTRY_NAME: "read_discrete_inputs", + ENTRY_FUNC: None, + }, + CALL_TYPE_REGISTER_HOLDING: { + ENTRY_ATTR: "registers", + ENTRY_NAME: "read_holding_registers", + ENTRY_FUNC: None, + }, + CALL_TYPE_REGISTER_INPUT: { + ENTRY_ATTR: "registers", + ENTRY_NAME: "read_input_registers", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_COIL: { + ENTRY_ATTR: "value", + ENTRY_NAME: "write_coil", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_COILS: { + ENTRY_ATTR: "count", + ENTRY_NAME: "write_coils", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_REGISTER: { + ENTRY_ATTR: "value", + ENTRY_NAME: "write_register", + ENTRY_FUNC: None, + }, + CALL_TYPE_WRITE_REGISTERS: { + ENTRY_ATTR: "count", + ENTRY_NAME: "write_registers", + ENTRY_FUNC: None, + }, +} + async def async_modbus_setup( hass, config, service_write_register_schema, service_write_coil_schema @@ -147,58 +194,39 @@ class ModbusHub: self.hass = hass self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] - self._config_port = client_config[CONF_PORT] - self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = client_config[CONF_DELAY] - self._config_reset_socket = client_config[CONF_CLOSE_COMM_ON_ERROR] - self._config_retries = client_config[CONF_RETRIES] - self._config_retry_on_empty = client_config[CONF_RETRY_ON_EMPTY] - Defaults.Timeout = client_config[CONF_TIMEOUT] - if self._config_type == "serial": + self._pb_call = PYMODBUS_CALL.copy() + self._pb_class = { + CONF_SERIAL: ModbusSerialClient, + CONF_TCP: ModbusTcpClient, + CONF_UDP: ModbusUdpClient, + CONF_RTUOVERTCP: ModbusTcpClient, + } + self._pb_params = { + "port": client_config[CONF_PORT], + "timeout": client_config[CONF_TIMEOUT], + "reset_socket": client_config[CONF_CLOSE_COMM_ON_ERROR], + "retries": client_config[CONF_RETRIES], + "retry_on_empty": client_config[CONF_RETRY_ON_EMPTY], + } + if self._config_type == CONF_SERIAL: # serial configuration - self._config_method = client_config[CONF_METHOD] - self._config_baudrate = client_config[CONF_BAUDRATE] - self._config_stopbits = client_config[CONF_STOPBITS] - self._config_bytesize = client_config[CONF_BYTESIZE] - self._config_parity = client_config[CONF_PARITY] + self._pb_params.update( + { + "method": client_config[CONF_METHOD], + "baudrate": client_config[CONF_BAUDRATE], + "stopbits": client_config[CONF_STOPBITS], + "bytesize": client_config[CONF_BYTESIZE], + "parity": client_config[CONF_PARITY], + } + ) else: # network configuration - self._config_host = client_config[CONF_HOST] + self._pb_params["host"] = client_config[CONF_HOST] + if self._config_type == CONF_RTUOVERTCP: + self._pb_params["host"] = "ModbusRtuFramer" - self._call_type = { - CALL_TYPE_COIL: { - ENTRY_ATTR: "bits", - ENTRY_FUNC: None, - }, - CALL_TYPE_DISCRETE: { - ENTRY_ATTR: "bits", - ENTRY_FUNC: None, - }, - CALL_TYPE_REGISTER_HOLDING: { - ENTRY_ATTR: "registers", - ENTRY_FUNC: None, - }, - CALL_TYPE_REGISTER_INPUT: { - ENTRY_ATTR: "registers", - ENTRY_FUNC: None, - }, - CALL_TYPE_WRITE_COIL: { - ENTRY_ATTR: "value", - ENTRY_FUNC: None, - }, - CALL_TYPE_WRITE_COILS: { - ENTRY_ATTR: "count", - ENTRY_FUNC: None, - }, - CALL_TYPE_WRITE_REGISTER: { - ENTRY_ATTR: "value", - ENTRY_FUNC: None, - }, - CALL_TYPE_WRITE_REGISTERS: { - ENTRY_ATTR: "count", - ENTRY_FUNC: None, - }, - } + Defaults.Timeout = client_config[CONF_TIMEOUT] def _log_error(self, text: str, error_state=True): log_text = f"Pymodbus: {text}" @@ -211,75 +239,19 @@ class ModbusHub: async def async_setup(self): """Set up pymodbus client.""" try: - if self._config_type == "serial": - self._client = ModbusSerialClient( - method=self._config_method, - port=self._config_port, - baudrate=self._config_baudrate, - stopbits=self._config_stopbits, - bytesize=self._config_bytesize, - parity=self._config_parity, - timeout=self._config_timeout, - retries=self._config_retries, - retry_on_empty=self._config_retry_on_empty, - reset_socket=self._config_reset_socket, - ) - elif self._config_type == "rtuovertcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - framer=ModbusRtuFramer, - timeout=self._config_timeout, - retries=self._config_retries, - retry_on_empty=self._config_retry_on_empty, - reset_socket=self._config_reset_socket, - ) - elif self._config_type == "tcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - retries=self._config_retries, - retry_on_empty=self._config_retry_on_empty, - reset_socket=self._config_reset_socket, - ) - elif self._config_type == "udp": - self._client = ModbusUdpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - retries=self._config_retries, - retry_on_empty=self._config_retry_on_empty, - reset_socket=self._config_reset_socket, - ) + self._client = self._pb_class[self._config_type](**self._pb_params) except ModbusException as exception_error: self._log_error(str(exception_error), error_state=False) return False + for entry in self._pb_call.values(): + entry[ENTRY_FUNC] = getattr(self._client, entry[ENTRY_NAME]) + async with self._lock: if not await self.hass.async_add_executor_job(self._pymodbus_connect): self._log_error("initial connect failed, no retry", error_state=False) return False - self._call_type[CALL_TYPE_COIL][ENTRY_FUNC] = self._client.read_coils - self._call_type[CALL_TYPE_DISCRETE][ - ENTRY_FUNC - ] = self._client.read_discrete_inputs - self._call_type[CALL_TYPE_REGISTER_HOLDING][ - ENTRY_FUNC - ] = self._client.read_holding_registers - self._call_type[CALL_TYPE_REGISTER_INPUT][ - ENTRY_FUNC - ] = self._client.read_input_registers - self._call_type[CALL_TYPE_WRITE_COIL][ENTRY_FUNC] = self._client.write_coil - self._call_type[CALL_TYPE_WRITE_COILS][ENTRY_FUNC] = self._client.write_coils - self._call_type[CALL_TYPE_WRITE_REGISTER][ - ENTRY_FUNC - ] = self._client.write_register - self._call_type[CALL_TYPE_WRITE_REGISTERS][ - ENTRY_FUNC - ] = self._client.write_registers - # Start counting down to allow modbus requests. if self._config_delay: self._async_cancel_listener = async_call_later( @@ -323,11 +295,11 @@ class ModbusHub: """Call sync. pymodbus.""" kwargs = {"unit": unit} if unit else {} try: - result = self._call_type[use_call][ENTRY_FUNC](address, value, **kwargs) + result = self._pb_call[use_call][ENTRY_FUNC](address, value, **kwargs) except ModbusException as exception_error: self._log_error(str(exception_error)) return None - if not hasattr(result, self._call_type[use_call][ENTRY_ATTR]): + if not hasattr(result, self._pb_call[use_call][ENTRY_ATTR]): self._log_error(str(result)) return None self._in_error = False From 054ca1d7ec73317a562fbcd8b7f380d2a6902608 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 11:51:55 +0200 Subject: [PATCH 422/750] Add Select entity component platform (#51849) Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/select.py | 80 +++++++++++++++ .../components/demo/strings.select.json | 9 ++ .../demo/translations/select.en.json | 9 ++ homeassistant/components/select/__init__.py | 98 +++++++++++++++++++ homeassistant/components/select/const.py | 8 ++ homeassistant/components/select/manifest.json | 7 ++ homeassistant/components/select/services.yaml | 14 +++ homeassistant/components/select/strings.json | 3 + mypy.ini | 11 +++ script/hassfest/manifest.py | 1 + tests/components/demo/test_select.py | 73 ++++++++++++++ tests/components/select/__init__.py | 1 + tests/components/select/test_init.py | 28 ++++++ 16 files changed, 345 insertions(+) create mode 100644 homeassistant/components/demo/select.py create mode 100644 homeassistant/components/demo/strings.select.json create mode 100644 homeassistant/components/demo/translations/select.en.json create mode 100644 homeassistant/components/select/__init__.py create mode 100644 homeassistant/components/select/const.py create mode 100644 homeassistant/components/select/manifest.json create mode 100644 homeassistant/components/select/services.yaml create mode 100644 homeassistant/components/select/strings.json create mode 100644 tests/components/demo/test_select.py create mode 100644 tests/components/select/__init__.py create mode 100644 tests/components/select/test_init.py diff --git a/.strict-typing b/.strict-typing index 2427bcdd754..d389d9e1950 100644 --- a/.strict-typing +++ b/.strict-typing @@ -61,6 +61,7 @@ homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.remote.* homeassistant.components.scene.* +homeassistant.components.select.* homeassistant.components.sensor.* homeassistant.components.slack.* homeassistant.components.sonos.media_player diff --git a/CODEOWNERS b/CODEOWNERS index 3f895d6572d..c19f4cbc721 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -423,6 +423,7 @@ homeassistant/components/scrape/* @fabaff homeassistant/components/screenlogic/* @dieselrabbit homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core +homeassistant/components/select/* @home-assistant/core homeassistant/components/sense/* @kbickar homeassistant/components/sensibo/* @andrey-git homeassistant/components/sentry/* @dcramer @frenck diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index b32537ae44e..acd98465207 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -20,6 +20,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "lock", "media_player", "number", + "select", "sensor", "switch", "vacuum", diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py new file mode 100644 index 00000000000..dcc0c12a9b4 --- /dev/null +++ b/homeassistant/components/demo/select.py @@ -0,0 +1,80 @@ +"""Demo platform that offers a fake select entity.""" +from __future__ import annotations + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: + """Set up the demo Select entity.""" + async_add_entities( + [ + DemoSelect( + unique_id="speed", + name="Speed", + icon="mdi:speedometer", + device_class="demo__speed", + current_option="ridiculous_speed", + options=[ + "light_speed", + "ridiculous_speed", + "ludicrous_speed", + ], + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoSelect(SelectEntity): + """Representation of a demo select entity.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + icon: str, + device_class: str | None, + current_option: str | None, + options: list[str], + ) -> None: + """Initialize the Demo select entity.""" + self._attr_unique_id = unique_id + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_current_option = current_option + self._attr_icon = icon + self._attr_device_class = device_class + self._attr_options = options + self._attr_device_info = { + "identifiers": {(DOMAIN, unique_id)}, + "name": name, + } + + async def async_select_option(self, option: str) -> None: + """Update the current selected option.""" + if option not in self.options: + raise ValueError(f"Invalid option for {self.entity_id}: {option}") + + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/demo/strings.select.json b/homeassistant/components/demo/strings.select.json new file mode 100644 index 00000000000..f797ab562bc --- /dev/null +++ b/homeassistant/components/demo/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Light Speed", + "ludicrous_speed": "Ludicrous Speed", + "ridiculous_speed": "Ridiculous Speed" + } + } +} diff --git a/homeassistant/components/demo/translations/select.en.json b/homeassistant/components/demo/translations/select.en.json new file mode 100644 index 00000000000..e7f7c67f452 --- /dev/null +++ b/homeassistant/components/demo/translations/select.en.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Light Speed", + "ludicrous_speed": "Ludicrous Speed", + "ridiculous_speed": "Ridiculous Speed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py new file mode 100644 index 00000000000..4ec8c46ef05 --- /dev/null +++ b/homeassistant/components/select/__init__.py @@ -0,0 +1,98 @@ +"""Component to allow selecting an option from a list as platforms.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType + +from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION + +SCAN_INTERVAL = timedelta(seconds=30) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Select entities.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_SELECT_OPTION, + {vol.Required(ATTR_OPTION): cv.string}, + "async_select_option", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +class SelectEntity(Entity): + """Representation of a Select entity.""" + + _attr_current_option: str | None + _attr_options: list[str] + _attr_state: None = None + + @property + def capability_attributes(self) -> dict[str, Any]: + """Return capability attributes.""" + return { + ATTR_OPTIONS: self.options, + } + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if self.current_option is None or self.current_option not in self.options: + return None + return self.current_option + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return self._attr_options + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self._attr_current_option + + def select_option(self, option: str) -> None: + """Change the selected option.""" + raise NotImplementedError() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.hass.async_add_executor_job(self.select_option, option) diff --git a/homeassistant/components/select/const.py b/homeassistant/components/select/const.py new file mode 100644 index 00000000000..41598c2edbc --- /dev/null +++ b/homeassistant/components/select/const.py @@ -0,0 +1,8 @@ +"""Provides the constants needed for the component.""" + +DOMAIN = "select" + +ATTR_OPTIONS = "options" +ATTR_OPTION = "option" + +SERVICE_SELECT_OPTION = "select_option" diff --git a/homeassistant/components/select/manifest.json b/homeassistant/components/select/manifest.json new file mode 100644 index 00000000000..86e8b917199 --- /dev/null +++ b/homeassistant/components/select/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "select", + "name": "Select", + "documentation": "https://www.home-assistant.io/integrations/select", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/select/services.yaml b/homeassistant/components/select/services.yaml new file mode 100644 index 00000000000..edf7fb50f00 --- /dev/null +++ b/homeassistant/components/select/services.yaml @@ -0,0 +1,14 @@ +select_option: + name: Select + description: Select an option of an select entity. + target: + entity: + domain: select + fields: + option: + name: Option + description: Option to be selected. + required: true + example: '"Item A"' + selector: + text: diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json new file mode 100644 index 00000000000..8e89a4faeb8 --- /dev/null +++ b/homeassistant/components/select/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Select" +} diff --git a/mypy.ini b/mypy.ini index b21c71d172c..52c48a54035 100644 --- a/mypy.ini +++ b/mypy.ini @@ -682,6 +682,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.select.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sensor.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index a8e1858cad3..00110e11fbc 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -91,6 +91,7 @@ NO_IOT_CLASS = [ "scene", "script", "search", + "select", "sensor", "stt", "switch", diff --git a/tests/components/demo/test_select.py b/tests/components/demo/test_select.py new file mode 100644 index 00000000000..628c173da7e --- /dev/null +++ b/tests/components/demo/test_select.py @@ -0,0 +1,73 @@ +"""The tests for the demo select component.""" + +import pytest + +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ENTITY_SPEED = "select.speed" + + +@pytest.fixture(autouse=True) +async def setup_demo_select(hass: HomeAssistant) -> None: + """Initialize setup demo select entity.""" + assert await async_setup_component(hass, DOMAIN, {"select": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get(ENTITY_SPEED) + assert state + assert state.state == "ridiculous_speed" + assert state.attributes.get(ATTR_OPTIONS) == [ + "light_speed", + "ridiculous_speed", + "ludicrous_speed", + ] + + +async def test_select_option_bad_attr(hass: HomeAssistant) -> None: + """Test selecting a different option with invalid option value.""" + state = hass.states.get(ENTITY_SPEED) + assert state + assert state.state == "ridiculous_speed" + + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "slow_speed", ATTR_ENTITY_ID: ENTITY_SPEED}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_SPEED) + assert state + assert state.state == "ridiculous_speed" + + +async def test_select_option(hass: HomeAssistant) -> None: + """Test selecting of a option.""" + state = hass.states.get(ENTITY_SPEED) + assert state + assert state.state == "ridiculous_speed" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "light_speed", ATTR_ENTITY_ID: ENTITY_SPEED}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_SPEED) + assert state + assert state.state == "light_speed" diff --git a/tests/components/select/__init__.py b/tests/components/select/__init__.py new file mode 100644 index 00000000000..1e7aecea908 --- /dev/null +++ b/tests/components/select/__init__.py @@ -0,0 +1 @@ +"""The tests for the Select integration.""" diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py new file mode 100644 index 00000000000..188099164c2 --- /dev/null +++ b/tests/components/select/test_init.py @@ -0,0 +1,28 @@ +"""The tests for the Select component.""" +from homeassistant.components.select import SelectEntity +from homeassistant.core import HomeAssistant + + +class MockSelectEntity(SelectEntity): + """Mock SelectEntity to use in tests.""" + + _attr_current_option = "option_one" + _attr_options = ["option_one", "option_two", "option_three"] + + +async def test_select(hass: HomeAssistant) -> None: + """Test getting data from the mocked select entity.""" + select = MockSelectEntity() + assert select.current_option == "option_one" + assert select.state == "option_one" + assert select.options == ["option_one", "option_two", "option_three"] + + # Test none selected + select._attr_current_option = None + assert select.current_option is None + assert select.state is None + + # Test none existing selected + select._attr_current_option = "option_four" + assert select.current_option == "option_four" + assert select.state is None From 2628ce54d9d1856c01cbae6a29d089f9b057da12 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 18 Jun 2021 18:46:20 +0200 Subject: [PATCH 423/750] Type homeassistant triggers event (#51979) --- .strict-typing | 1 + .../homeassistant/triggers/event.py | 25 +++++++++++------ mypy.ini | 28 ++++++++++++++++++- script/hassfest/mypy_config.py | 7 ++++- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index d389d9e1950..c87b0b270a8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -35,6 +35,7 @@ homeassistant.components.geo_location.* homeassistant.components.gios.* homeassistant.components.group.* homeassistant.components.history.* +homeassistant.components.homeassistant.triggers.event homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 6b4cf520560..47dc5317bbd 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -1,11 +1,15 @@ """Offer event listening automation rules.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template - -# mypy: allow-untyped-defs +from homeassistant.helpers.typing import ConfigType CONF_EVENT_TYPE = "event_type" CONF_EVENT_CONTEXT = "context" @@ -20,7 +24,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -def _schema_value(value): +def _schema_value(value: Any) -> Any: if isinstance(value, list): return vol.In(value) @@ -28,8 +32,13 @@ def _schema_value(value): async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type="event" -): + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict[str, Any], + *, + platform_type: str = "event", +) -> CALLBACK_TYPE: """Listen for events based on configuration.""" trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} variables = None @@ -76,7 +85,7 @@ async def async_attach_trigger( job = HassJob(action) @callback - def handle_event(event): + def handle_event(event: Event) -> None: """Listen for events and calls the action when data matches.""" try: # Check that the event data and context match the configured @@ -107,7 +116,7 @@ async def async_attach_trigger( ] @callback - def remove_listen_events(): + def remove_listen_events() -> None: """Remove event listeners.""" for remove in removes: remove() diff --git a/mypy.ini b/mypy.ini index 52c48a54035..f80f2baa9e3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -396,6 +396,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant.triggers.event] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.http.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1148,7 +1159,22 @@ ignore_errors = true [mypy-homeassistant.components.home_plus_control.*] ignore_errors = true -[mypy-homeassistant.components.homeassistant.*] +[mypy-homeassistant.components.homeassistant.triggers.homeassistant] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.triggers.numeric_state] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.triggers.time_pattern] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.triggers.time] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.triggers.state] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.scene] ignore_errors = true [mypy-homeassistant.components.homekit.*] diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e9567be8924..b934bed3bc1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -85,7 +85,12 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.hisense_aehw4a1.*", "homeassistant.components.home_connect.*", "homeassistant.components.home_plus_control.*", - "homeassistant.components.homeassistant.*", + "homeassistant.components.homeassistant.triggers.homeassistant", + "homeassistant.components.homeassistant.triggers.numeric_state", + "homeassistant.components.homeassistant.triggers.time_pattern", + "homeassistant.components.homeassistant.triggers.time", + "homeassistant.components.homeassistant.triggers.state", + "homeassistant.components.homeassistant.scene", "homeassistant.components.homekit.*", "homeassistant.components.homekit_controller.*", "homeassistant.components.homematicip_cloud.*", From c1cfbcc4e3f330edd55ba28597426262919bae0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 19:01:32 +0200 Subject: [PATCH 424/750] Add device trigger support to Select entity (#51987) --- .../components/select/device_trigger.py | 103 +++++++++ homeassistant/components/select/strings.json | 7 +- .../components/select/translations/en.json | 8 + .../components/select/test_device_trigger.py | 217 ++++++++++++++++++ 4 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/select/device_trigger.py create mode 100644 homeassistant/components/select/translations/en.json create mode 100644 tests/components/select/test_device_trigger.py diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py new file mode 100644 index 00000000000..164f420c122 --- /dev/null +++ b/homeassistant/components/select/device_trigger.py @@ -0,0 +1,103 @@ +"""Provides device triggers for Select.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.homeassistant.triggers.state import ( + CONF_FOR, + CONF_FROM, + CONF_TO, + TRIGGER_SCHEMA as STATE_TRIGGER_SCHEMA, + async_attach_trigger as async_attach_state_trigger, +) +from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = {"current_option_changed"} + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_TO): vol.Any(vol.Coerce(str)), + vol.Optional(CONF_FROM): vol.Any(vol.Coerce(str)), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device triggers for Select devices.""" + registry = await entity_registry.async_get_registry(hass) + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "current_option_changed", + } + for entry in entity_registry.async_entries_for_device(registry, device_id) + if entry.domain == DOMAIN + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + state_config = { + CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + if CONF_TO in config: + state_config[CONF_TO] = config[CONF_TO] + + if CONF_FROM in config: + state_config[CONF_FROM] = config[CONF_FROM] + + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + + state_config = STATE_TRIGGER_SCHEMA(state_config) + return await async_attach_state_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, Any]: + """List trigger capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + if state is None: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_FROM): vol.In(state.attributes.get(ATTR_OPTIONS, [])), + vol.Optional(CONF_TO): vol.In(state.attributes.get(ATTR_OPTIONS, [])), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 8e89a4faeb8..58b51978f35 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -1,3 +1,8 @@ { - "title": "Select" + "title": "Select", + "device_automation": { + "trigger_type": { + "current_option_changed": "{entity_name} option changed" + } + } } diff --git a/homeassistant/components/select/translations/en.json b/homeassistant/components/select/translations/en.json new file mode 100644 index 00000000000..282c8fddd83 --- /dev/null +++ b/homeassistant/components/select/translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "current_option_changed": "{entity_name} option changed" + } + }, + "title": "Select" +} \ No newline at end of file diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py new file mode 100644 index 00000000000..df81a67b847 --- /dev/null +++ b/tests/components/select/test_device_trigger.py @@ -0,0 +1,217 @@ +"""The tests for Select device triggers.""" +from __future__ import annotations + +import pytest +import voluptuous_serialize + +from homeassistant.components import automation +from homeassistant.components.select import DOMAIN +from homeassistant.components.select.device_trigger import ( + async_get_trigger_capabilities, +) +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry: + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant) -> EntityRegistry: + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass: HomeAssistant) -> list[ServiceCall]: + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers( + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: EntityRegistry, +) -> None: + """Test we get the expected triggers from a select.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "current_option_changed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set( + "select.entity", "option1", {"options": ["option1", "option2", "option3"]} + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "select.entity", + "type": "current_option_changed", + "to": "option2", + }, + "action": { + "service": "test.automation", + "data": { + "some": ( + "to - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }} - " + "{{ trigger.id}}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "select.entity", + "type": "current_option_changed", + "from": "option2", + }, + "action": { + "service": "test.automation", + "data": { + "some": ( + "from - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }} - " + "{{ trigger.id}}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "select.entity", + "type": "current_option_changed", + "from": "option3", + "to": "option1", + }, + "action": { + "service": "test.automation", + "data": { + "some": ( + "from-to - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }} - " + "{{ trigger.id}}" + ) + }, + }, + }, + ] + }, + ) + + # Test triggering device trigger with a to state + hass.states.async_set("select.entity", "option2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "to - device - {} - option1 - option2 - None - 0".format("select.entity") + + # Test triggering device trigger with a from state + hass.states.async_set("select.entity", "option3") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "from - device - {} - option2 - option3 - None - 0".format("select.entity") + + # Test triggering device trigger with both a from and to state + hass.states.async_set("select.entity", "option1") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data[ + "some" + ] == "from-to - device - {} - option3 - option1 - None - 0".format("select.entity") + + +async def test_get_trigger_capabilities(hass: HomeAssistant) -> None: + """Test we get the expected capabilities from a select trigger.""" + config = { + "platform": "device", + "domain": DOMAIN, + "type": "current_option_changed", + "entity_id": "select.test", + "to": "option1", + } + + # Test when entity doesn't exists + capabilities = await async_get_trigger_capabilities(hass, config) + assert capabilities == {} + + # Mock an entity + hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) + + # Test if we get the right capabilities now + capabilities = await async_get_trigger_capabilities(hass, config) + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "from", + "optional": True, + "type": "select", + "options": [("option1", "option1"), ("option2", "option2")], + }, + { + "name": "to", + "optional": True, + "type": "select", + "options": [("option1", "option1"), ("option2", "option2")], + }, + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + "optional": True, + }, + ] From 98a53188f8f9cc8a2e37fcfb93ee20fd18618135 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 20:30:40 +0200 Subject: [PATCH 425/750] Add reproduce state to select entity (#51977) --- .../components/select/reproduce_state.py | 66 +++++++++++++++++++ .../components/select/test_reproduce_state.py | 57 ++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 homeassistant/components/select/reproduce_state.py create mode 100644 tests/components/select/test_reproduce_state.py diff --git a/homeassistant/components/select/reproduce_state.py b/homeassistant/components/select/reproduce_state.py new file mode 100644 index 00000000000..8af4b94fd6f --- /dev/null +++ b/homeassistant/components/select/reproduce_state.py @@ -0,0 +1,66 @@ +"""Reproduce a Select entity state.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +import logging +from typing import Any + +from homeassistant.components.select.const import ATTR_OPTIONS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant, State + +from . import ATTR_OPTION, DOMAIN, SERVICE_SELECT_OPTION + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistant, + state: State, + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in cur_state.attributes.get(ATTR_OPTIONS, []): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: state.state}, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistant, + states: Iterable[State], + *, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, +) -> None: + """Reproduce multiple select states.""" + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/tests/components/select/test_reproduce_state.py b/tests/components/select/test_reproduce_state.py new file mode 100644 index 00000000000..b1ab3a0a5aa --- /dev/null +++ b/tests/components/select/test_reproduce_state.py @@ -0,0 +1,57 @@ +"""Test reproduce state for select entities.""" +import pytest + +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, State + +from tests.common import async_mock_service + + +async def test_reproducing_states( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test reproducing select states.""" + calls = async_mock_service(hass, DOMAIN, SERVICE_SELECT_OPTION) + hass.states.async_set( + "select.test", + "option_one", + {ATTR_OPTIONS: ["option_one", "option_two", "option_three"]}, + ) + + await hass.helpers.state.async_reproduce_state( + [ + State("select.test", "option_two"), + ], + ) + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].data == {ATTR_ENTITY_ID: "select.test", ATTR_OPTION: "option_two"} + + # Calling it again should not do anything + await hass.helpers.state.async_reproduce_state( + [ + State("select.test", "option_one"), + ], + ) + assert len(calls) == 1 + + # Restoring an invalid state should not work either + await hass.helpers.state.async_reproduce_state( + [State("select.test", "option_four")] + ) + assert len(calls) == 1 + assert "Invalid state specified" in caplog.text + + # Restoring an state for an invalid entity ID logs a warning + await hass.helpers.state.async_reproduce_state( + [State("select.non_existing", "option_three")] + ) + assert len(calls) == 1 + assert "Unable to find entity" in caplog.text From 06edc731c5fc937baa59ad57645b62e8eae95e32 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 20:31:09 +0200 Subject: [PATCH 426/750] Add significant change support to select entity (#51978) --- .../components/select/significant_change.py | 19 ++++++++++++++++++ .../select/test_significant_change.py | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 homeassistant/components/select/significant_change.py create mode 100644 tests/components/select/test_significant_change.py diff --git a/homeassistant/components/select/significant_change.py b/homeassistant/components/select/significant_change.py new file mode 100644 index 00000000000..835db314a38 --- /dev/null +++ b/homeassistant/components/select/significant_change.py @@ -0,0 +1,19 @@ +"""Helper to test significant Select state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> bool: + """Test if state significantly changed.""" + return old_state != new_state diff --git a/tests/components/select/test_significant_change.py b/tests/components/select/test_significant_change.py new file mode 100644 index 00000000000..34ae5cad54e --- /dev/null +++ b/tests/components/select/test_significant_change.py @@ -0,0 +1,20 @@ +"""Test the select significant change platform.""" +from homeassistant.components.select.significant_change import ( + async_check_significant_change, +) +from homeassistant.core import HomeAssistant + + +async def test_significant_change(hass: HomeAssistant) -> None: + """Detect select significant change.""" + attrs1 = {"options": ["option1", "option2"]} + attrs2 = {"options": ["option1", "option2", "option3"]} + + assert not async_check_significant_change( + hass, "option1", attrs1, "option1", attrs1 + ) + assert not async_check_significant_change( + hass, "option1", attrs1, "option1", attrs2 + ) + assert async_check_significant_change(hass, "option1", attrs1, "option2", attrs1) + assert async_check_significant_change(hass, "option1", attrs1, "option2", attrs2) From 87a43eacb70af6c43e6ef8067fe0a37e636d8b58 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 20:40:29 +0200 Subject: [PATCH 427/750] Add device action support to Select entity (#51990) --- homeassistant/components/select/const.py | 2 + .../components/select/device_action.py | 80 +++++++++++ homeassistant/components/select/strings.json | 3 + .../components/select/translations/en.json | 3 + tests/components/select/test_device_action.py | 127 ++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 homeassistant/components/select/device_action.py create mode 100644 tests/components/select/test_device_action.py diff --git a/homeassistant/components/select/const.py b/homeassistant/components/select/const.py index 41598c2edbc..1f4615b0b05 100644 --- a/homeassistant/components/select/const.py +++ b/homeassistant/components/select/const.py @@ -5,4 +5,6 @@ DOMAIN = "select" ATTR_OPTIONS = "options" ATTR_OPTION = "option" +CONF_OPTION = "option" + SERVICE_SELECT_OPTION = "select_option" diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py new file mode 100644 index 00000000000..656fca32970 --- /dev/null +++ b/homeassistant/components/select/device_action.py @@ -0,0 +1,80 @@ +"""Provides device actions for Select.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ATTR_OPTION, ATTR_OPTIONS, CONF_OPTION, DOMAIN, SERVICE_SELECT_OPTION + +ACTION_TYPES = {"select_option"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(CONF_OPTION): str, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: + """List device actions for Select devices.""" + registry = await entity_registry.async_get_registry(hass) + return [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "select_option", + } + for entry in entity_registry.async_entries_for_device(registry, device_id) + if entry.domain == DOMAIN + ] + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Context | None +) -> None: + """Execute a device action.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: config[CONF_ENTITY_ID], + ATTR_OPTION: config[CONF_OPTION], + }, + blocking=True, + context=context, + ) + + +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, Any]: + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + if state is None: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Required(CONF_OPTION): vol.In( + state.attributes.get(ATTR_OPTIONS, []) + ), + } + ) + } diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 58b51978f35..3a5424a559b 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -3,6 +3,9 @@ "device_automation": { "trigger_type": { "current_option_changed": "{entity_name} option changed" + }, + "action_type": { + "select_option": "Change {entity_name} option" } } } diff --git a/homeassistant/components/select/translations/en.json b/homeassistant/components/select/translations/en.json index 282c8fddd83..ff836198b96 100644 --- a/homeassistant/components/select/translations/en.json +++ b/homeassistant/components/select/translations/en.json @@ -1,5 +1,8 @@ { "device_automation": { + "action_type": { + "select_option": "Change {entity_name} option" + }, "trigger_type": { "current_option_changed": "{entity_name} option changed" } diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py new file mode 100644 index 00000000000..1cdffe9ae00 --- /dev/null +++ b/tests/components/select/test_device_action.py @@ -0,0 +1,127 @@ +"""The tests for Select device actions.""" +import pytest +import voluptuous_serialize + +from homeassistant.components import automation +from homeassistant.components.select import DOMAIN +from homeassistant.components.select.device_action import async_get_action_capabilities +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_registry, +) +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry: + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry: + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions( + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: entity_registry.EntityRegistry, +) -> None: + """Test we get the expected actions from a select.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "select_option", + "device_id": device_entry.id, + "entity_id": "select.test_5678", + } + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass: HomeAssistant) -> None: + """Test for select_option action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "select.entity", + "type": "select_option", + "option": "option1", + }, + }, + ] + }, + ) + + select_calls = async_mock_service(hass, DOMAIN, "select_option") + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(select_calls) == 1 + assert select_calls[0].domain == DOMAIN + assert select_calls[0].service == "select_option" + assert select_calls[0].data == {"entity_id": "select.entity", "option": "option1"} + + +async def test_get_trigger_capabilities(hass: HomeAssistant) -> None: + """Test we get the expected capabilities from a select action.""" + config = { + "platform": "device", + "domain": DOMAIN, + "type": "select_option", + "entity_id": "select.test", + "option": "option1", + } + + # Test when entity doesn't exists + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities == {} + + # Mock an entity + hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) + + # Test if we get the right capabilities now + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "option", + "required": True, + "type": "select", + "options": [("option1", "option1"), ("option2", "option2")], + }, + ] From 805ef3f90b24279b634f48a33366c4c4852aed65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Jun 2021 12:03:13 -0700 Subject: [PATCH 428/750] Allow fetching multiple statistics (#51996) --- homeassistant/components/history/__init__.py | 6 +-- .../components/recorder/statistics.py | 14 +++---- tests/components/history/test_init.py | 38 ++++++++----------- tests/components/recorder/test_statistics.py | 31 +++++++-------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index ac8a13e69ad..2b30936d9ed 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -119,7 +119,7 @@ class LazyState(history_models.LazyState): vol.Required("type"): "history/statistics_during_period", vol.Required("start_time"): str, vol.Optional("end_time"): str, - vol.Optional("statistic_id"): str, + vol.Optional("statistic_ids"): [str], } ) @websocket_api.async_response @@ -152,9 +152,9 @@ async def ws_get_statistics_during_period( hass, start_time, end_time, - msg.get("statistic_id"), + msg.get("statistic_ids"), ) - connection.send_result(msg["id"], {"statistics": statistics}) + connection.send_result(msg["id"], statistics) class HistoryPeriodView(HomeAssistantView): diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ee733039d43..1e7f37f9f47 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -74,7 +74,7 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: return True -def statistics_during_period(hass, start_time, end_time=None, statistic_id=None): +def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: baked_query = hass.data[STATISTICS_BAKERY]( @@ -86,20 +86,20 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_id=None) if end_time is not None: baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time")) - if statistic_id is not None: - baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) - statistic_id = statistic_id.lower() + if statistic_ids is not None: + baked_query += lambda q: q.filter( + Statistics.statistic_id.in_(bindparam("statistic_ids")) + ) + statistic_ids = [statistic_id.lower() for statistic_id in statistic_ids] baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start) stats = execute( baked_query(session).params( - start_time=start_time, end_time=end_time, statistic_id=statistic_id + start_time=start_time, end_time=end_time, statistic_ids=statistic_ids ) ) - statistic_ids = [statistic_id] if statistic_id is not None else None - return _sorted_statistics_to_dict(stats, statistic_ids) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index bf8d34e6ffe..d47d3d80393 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -834,11 +834,7 @@ async def test_statistics_during_period(hass, hass_ws_client): now = dt_util.utcnow() await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component( - hass, - "history", - {"history": {}}, - ) + await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) hass.states.async_set( @@ -861,12 +857,12 @@ async def test_statistics_during_period(hass, hass_ws_client): "type": "history/statistics_during_period", "start_time": now.isoformat(), "end_time": now.isoformat(), - "statistic_id": "sensor.test", + "statistic_ids": ["sensor.test"], } ) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistics": {}} + assert response["result"] == {} client = await hass_ws_client() await client.send_json( @@ -874,26 +870,24 @@ async def test_statistics_during_period(hass, hass_ws_client): "id": 1, "type": "history/statistics_during_period", "start_time": now.isoformat(), - "statistic_id": "sensor.test", + "statistic_ids": ["sensor.test"], } ) response = await client.receive_json() assert response["success"] assert response["result"] == { - "statistics": { - "sensor.test": [ - { - "statistic_id": "sensor.test", - "start": now.isoformat(), - "mean": approx(10.0), - "min": approx(10.0), - "max": approx(10.0), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "mean": approx(10.0), + "min": approx(10.0), + "max": approx(10.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] } diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 1ec0f2284b4..679ef7597c2 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -26,21 +26,22 @@ def test_compile_hourly_statistics(hass_recorder): recorder.do_adhoc_statistics(period="hourly", start=zero) wait_recording_done(hass) - stats = statistics_during_period(hass, zero) - assert stats == { - "sensor.test1": [ - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "mean": approx(14.915254237288135), - "min": approx(10.0), - "max": approx(20.0), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } + for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): + stats = statistics_during_period(hass, zero, **kwargs) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } def record_states(hass): From 0ca199d8d03278916588cf7ed64c783b38c9b5ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Jun 2021 21:32:30 +0200 Subject: [PATCH 429/750] Add WS API for listing available statistic ids (#51984) * Add WS API for listing available statistic ids * Update homeassistant/components/history/__init__.py Co-authored-by: Paulus Schoutsen Co-authored-by: Bram Kragten --- homeassistant/components/history/__init__.py | 26 ++++++++- .../components/recorder/statistics.py | 24 +++++++++ tests/components/history/test_init.py | 54 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 2b30936d9ed..79d288398c5 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -14,7 +14,10 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import history, models as history_models -from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.components.recorder.statistics import ( + list_statistic_ids, + statistics_during_period, +) from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( CONF_DOMAINS, @@ -105,6 +108,7 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( ws_get_statistics_during_period ) + hass.components.websocket_api.async_register_command(ws_get_list_statistic_ids) return True @@ -157,6 +161,26 @@ async def ws_get_statistics_during_period( connection.send_result(msg["id"], statistics) +@websocket_api.websocket_command( + { + vol.Required("type"): "history/list_statistic_ids", + vol.Optional("statistic_type"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def ws_get_list_statistic_ids( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fetch a list of available statistic_id.""" + statistics = await hass.async_add_executor_job( + list_statistic_ids, + hass, + msg.get("statistic_type"), + ) + connection.send_result(msg["id"], {"statistic_ids": statistics}) + + class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 1e7f37f9f47..cd2b72ad43a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -30,6 +30,10 @@ QUERY_STATISTICS = [ Statistics.sum, ] +QUERY_STATISTIC_IDS = [ + Statistics.statistic_id, +] + STATISTICS_BAKERY = "recorder_statistics_bakery" _LOGGER = logging.getLogger(__name__) @@ -74,6 +78,26 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: return True +def list_statistic_ids(hass, statistic_type=None): + """Return statistic_ids.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[STATISTICS_BAKERY]( + lambda session: session.query(*QUERY_STATISTIC_IDS).distinct() + ) + + if statistic_type == "mean": + baked_query += lambda q: q.filter(Statistics.mean.isnot(None)) + if statistic_type == "sum": + baked_query += lambda q: q.filter(Statistics.sum.isnot(None)) + + baked_query += lambda q: q.order_by(Statistics.statistic_id) + + statistic_ids = [] + result = execute(baked_query(session)) + statistic_ids = [statistic_id[0] for statistic_id in result] + return statistic_ids + + def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index d47d3d80393..32205512474 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -938,3 +938,57 @@ async def test_statistics_during_period_bad_end_time(hass, hass_ws_client): response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "invalid_end_time" + + +async def test_list_statistic_ids(hass, hass_ws_client): + """Test list_statistic_ids.""" + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {"history": {}}) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set( + "sensor.test", + 10, + attributes={"device_class": "temperature", "state_class": "measurement"}, + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistic_ids": []} + + hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistic_ids": ["sensor.test"]} + + await client.send_json( + {"id": 3, "type": "history/list_statistic_ids", "statistic_type": "dogs"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistic_ids": ["sensor.test"]} + + await client.send_json( + {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "mean"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistic_ids": ["sensor.test"]} + + await client.send_json( + {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "sum"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistic_ids": []} From 655f797f67833f3b096b0d95096aa7e55c06037f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 23:30:46 +0200 Subject: [PATCH 430/750] Add Select entity support to Google Assistant (#51997) --- .../components/google_assistant/const.py | 13 ++-- .../components/google_assistant/trait.py | 21 ++++++ homeassistant/setup.py | 1 + .../components/google_assistant/test_trait.py | 75 +++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 3294ff54c2e..2e43e20f124 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -15,6 +15,7 @@ from homeassistant.components import ( media_player, scene, script, + select, sensor, switch, vacuum, @@ -39,6 +40,8 @@ CONF_PRIVATE_KEY = "private_key" DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ + "alarm_control_panel", + "binary_sensor", "climate", "cover", "fan", @@ -47,15 +50,14 @@ DEFAULT_EXPOSED_DOMAINS = [ "input_boolean", "input_select", "light", + "lock", "media_player", "scene", "script", + "select", + "sensor", "switch", "vacuum", - "lock", - "binary_sensor", - "sensor", - "alarm_control_panel", ] PREFIX_TYPES = "action.devices.types." @@ -117,6 +119,7 @@ EVENT_QUERY_RECEIVED = "google_assistant_query" EVENT_SYNC_RECEIVED = "google_assistant_sync" DOMAIN_TO_GOOGLE_TYPES = { + alarm_control_panel.DOMAIN: TYPE_ALARM, camera.DOMAIN: TYPE_CAMERA, climate.DOMAIN: TYPE_THERMOSTAT, cover.DOMAIN: TYPE_BLINDS, @@ -130,9 +133,9 @@ DOMAIN_TO_GOOGLE_TYPES = { media_player.DOMAIN: TYPE_SETTOP, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, + select.DOMAIN: TYPE_SENSOR, switch.DOMAIN: TYPE_SWITCH, vacuum.DOMAIN: TYPE_VACUUM, - alarm_control_panel.DOMAIN: TYPE_ALARM, } DEVICE_CLASS_TO_GOOGLE_TYPES = { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index d7cd55b7f80..0c547f18741 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -17,6 +17,7 @@ from homeassistant.components import ( media_player, scene, script, + select, sensor, switch, vacuum, @@ -1384,6 +1385,9 @@ class ModesTrait(_Trait): if domain == input_select.DOMAIN: return True + if domain == select.DOMAIN: + return True + if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES: return True @@ -1427,6 +1431,7 @@ class ModesTrait(_Trait): (fan.DOMAIN, fan.ATTR_PRESET_MODES, "preset mode"), (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"), (input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"), + (select.DOMAIN, select.ATTR_OPTIONS, "option"), (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"), (light.DOMAIN, light.ATTR_EFFECT_LIST, "effect"), ): @@ -1459,6 +1464,8 @@ class ModesTrait(_Trait): mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) elif self.state.domain == input_select.DOMAIN: mode_settings["option"] = self.state.state + elif self.state.domain == select.DOMAIN: + mode_settings["option"] = self.state.state elif self.state.domain == humidifier.DOMAIN: if ATTR_MODE in attrs: mode_settings["mode"] = attrs.get(ATTR_MODE) @@ -1503,6 +1510,20 @@ class ModesTrait(_Trait): ) return + if self.state.domain == select.DOMAIN: + option = settings["option"] + await self.hass.services.async_call( + select.DOMAIN, + select.SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: self.state.entity_id, + select.ATTR_OPTION: option, + }, + blocking=True, + context=data.context, + ) + return + if self.state.domain == humidifier.DOMAIN: requested_mode = settings["mode"] await self.hass.services.async_call( diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f5a6f9b9721..9b0c5282108 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -41,6 +41,7 @@ BASE_PLATFORMS = { "notify", "remote", "scene", + "select", "sensor", "switch", "tts", diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 46d81443a05..e2821d207d5 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -18,6 +18,7 @@ from homeassistant.components import ( media_player, scene, script, + select, sensor, switch, vacuum, @@ -1799,6 +1800,80 @@ async def test_modes_input_select(hass): assert calls[0].data == {"entity_id": "input_select.bla", "option": "xyz"} +async def test_modes_select(hass): + """Test Select Mode trait.""" + assert helpers.get_google_type(select.DOMAIN, None) is not None + assert trait.ModesTrait.supported(select.DOMAIN, None, None, None) + + trt = trait.ModesTrait( + hass, + State("select.bla", "unavailable"), + BASIC_CONFIG, + ) + assert trt.sync_attributes() == {"availableModes": []} + + trt = trait.ModesTrait( + hass, + State( + "select.bla", + "abc", + attributes={select.ATTR_OPTIONS: ["abc", "123", "xyz"]}, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "option", + "name_values": [ + { + "name_synonym": ["option", "setting", "mode", "value"], + "lang": "en", + } + ], + "settings": [ + { + "setting_name": "abc", + "setting_values": [{"setting_synonym": ["abc"], "lang": "en"}], + }, + { + "setting_name": "123", + "setting_values": [{"setting_synonym": ["123"], "lang": "en"}], + }, + { + "setting_name": "xyz", + "setting_values": [{"setting_synonym": ["xyz"], "lang": "en"}], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"option": "abc"}, + "on": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, + params={"updateModeSettings": {"option": "xyz"}}, + ) + + calls = async_mock_service(hass, select.DOMAIN, select.SERVICE_SELECT_OPTION) + await trt.execute( + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"option": "xyz"}}, + {}, + ) + + assert len(calls) == 1 + assert calls[0].data == {"entity_id": "select.bla", "option": "xyz"} + + async def test_modes_humidifier(hass): """Test Humidifier Mode trait.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None From 23222589ddc724dc346f4e37f9e777eebf84468a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Jun 2021 23:31:08 +0200 Subject: [PATCH 431/750] Add device condition support to Select entity (#51992) --- .../components/select/device_condition.py | 88 +++++++++ homeassistant/components/select/strings.json | 3 + .../components/select/translations/en.json | 3 + .../select/test_device_condition.py | 185 ++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 homeassistant/components/select/device_condition.py create mode 100644 tests/components/select/test_device_condition.py diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py new file mode 100644 index 00000000000..444105075b1 --- /dev/null +++ b/homeassistant/components/select/device_condition.py @@ -0,0 +1,88 @@ +"""Provide the device conditions for Select.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from .const import ATTR_OPTIONS, CONF_OPTION, DOMAIN + +CONDITION_TYPES = {"selected_option"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + vol.Required(CONF_OPTION): str, + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: + """List device conditions for Select devices.""" + registry = await entity_registry.async_get_registry(hass) + return [ + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "selected_option", + } + for entry in entity_registry.async_entries_for_device(registry, device_id) + if entry.domain == DOMAIN + ] + + +@callback +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + @callback + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state( + hass, config[CONF_ENTITY_ID], config[CONF_OPTION], config.get(CONF_FOR) + ) + + return test_is_state + + +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, Any]: + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + if state is None: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Required(CONF_OPTION): vol.In( + state.attributes.get(ATTR_OPTIONS, []) + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 3a5424a559b..5724ff67a14 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -6,6 +6,9 @@ }, "action_type": { "select_option": "Change {entity_name} option" + }, + "condition_type": { + "selected_option": "Current {entity_name} selected option" } } } diff --git a/homeassistant/components/select/translations/en.json b/homeassistant/components/select/translations/en.json index ff836198b96..7f126ef36e8 100644 --- a/homeassistant/components/select/translations/en.json +++ b/homeassistant/components/select/translations/en.json @@ -3,6 +3,9 @@ "action_type": { "select_option": "Change {entity_name} option" }, + "condition_type": { + "selected_option": "Current {entity_name} selected option" + }, "trigger_type": { "current_option_changed": "{entity_name} option changed" } diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py new file mode 100644 index 00000000000..fa2d2736e0f --- /dev/null +++ b/tests/components/select/test_device_condition.py @@ -0,0 +1,185 @@ +"""The tests for Select device conditions.""" +from __future__ import annotations + +import pytest +import voluptuous_serialize + +from homeassistant.components import automation +from homeassistant.components.select import DOMAIN +from homeassistant.components.select.device_condition import ( + async_get_condition_capabilities, +) +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import ( + config_validation as cv, + device_registry, + entity_registry, +) +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass: HomeAssistant) -> device_registry.DeviceRegistry: + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant) -> entity_registry.EntityRegistry: + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass: HomeAssistant) -> list[ServiceCall]: + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions( + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: entity_registry.EntityRegistry, +) -> None: + """Test we get the expected conditions from a select.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "selected_option", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_selected_option( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test for selected_option conditions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "select.entity", + "type": "selected_option", + "option": "option1", + } + ], + "action": { + "service": "test.automation", + "data": { + "result": "option1 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "select.entity", + "type": "selected_option", + "option": "option2", + } + ], + "action": { + "service": "test.automation", + "data": { + "result": "option2 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + + # Test with non existing entity + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set( + "select.entity", "option1", {"options": ["option1", "option2"]} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["result"] == "option1 - event - test_event1" + + hass.states.async_set( + "select.entity", "option2", {"options": ["option1", "option2"]} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["result"] == "option2 - event - test_event2" + + +async def test_get_condition_capabilities(hass: HomeAssistant) -> None: + """Test we get the expected capabilities from a select condition.""" + config = { + "platform": "device", + "domain": DOMAIN, + "type": "selected_option", + "entity_id": "select.test", + "option": "option1", + } + + # Test when entity doesn't exists + capabilities = await async_get_condition_capabilities(hass, config) + assert capabilities == {} + + # Mock an entity + hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) + + # Test if we get the right capabilities now + capabilities = await async_get_condition_capabilities(hass, config) + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "option", + "required": True, + "type": "select", + "options": [("option1", "option1"), ("option2", "option2")], + }, + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + }, + ] From 549f779b0679b004f67aca996b107414355a6e36 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 18 Jun 2021 16:11:35 -0600 Subject: [PATCH 432/750] Force SimpliSafe to reauthenticate with a password (#51528) --- .../components/simplisafe/__init__.py | 79 +++++++------------ .../components/simplisafe/config_flow.py | 13 +-- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- .../components/simplisafe/test_config_flow.py | 20 +++-- 5 files changed, 54 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c49aeb065e4..b7c2a08f093 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -6,7 +6,7 @@ from simplipy import API from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -107,14 +107,6 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -@callback -def _async_save_refresh_token(hass, config_entry, token): - """Save a refresh token to the config entry.""" - hass.config_entries.async_update_entry( - config_entry, data={**config_entry.data, CONF_TOKEN: token} - ) - - async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -136,16 +128,15 @@ async def async_register_base_station(hass, system, config_entry_id): ) -async def async_setup(hass, config): - """Set up the SimpliSafe component.""" - hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} - return True - - async def async_setup_entry(hass, config_entry): # noqa: C901 """Set up SimpliSafe as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] + if CONF_PASSWORD not in config_entry.data: + raise ConfigEntryAuthFailed("Config schema change requires re-authentication") + entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -167,20 +158,24 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) - try: - api = await API.login_via_token( - config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + async def async_get_api(): + """Define a helper to get an authenticated SimpliSafe API object.""" + return await API.login_via_credentials( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + client_id=client_id, + session=websession, ) - except InvalidCredentialsError: - LOGGER.error("Invalid credentials provided") - return False + + try: + api = await async_get_api() + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - _async_save_refresh_token(hass, config_entry, api.refresh_token) - - simplisafe = SimpliSafe(hass, api, config_entry) + simplisafe = SimpliSafe(hass, config_entry, api, async_get_api) try: await simplisafe.async_init() @@ -307,10 +302,10 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, api, config_entry): + def __init__(self, hass, config_entry, api, async_get_api): """Initialize.""" self._api = api - self._emergency_refresh_token_used = False + self._async_get_api = async_get_api self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -387,23 +382,17 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - if self._emergency_refresh_token_used: - raise ConfigEntryAuthFailed( - "Update failed with stored refresh token" - ) - - LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") - self._emergency_refresh_token_used = True - try: - await self._api.refresh_access_token( - self.config_entry.data[CONF_TOKEN] - ) + self._api = await self._async_get_api() return + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed( + "Unable to re-authenticate with SimpliSafe" + ) from err except SimplipyError as err: - raise UpdateFailed( # pylint: disable=raise-missing-from - f"Error while using stored refresh token: {err}" - ) + raise UpdateFailed( + f"SimpliSafe error while updating: {err}" + ) from err if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -414,16 +403,6 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") - if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: - _async_save_refresh_token( - self._hass, self.config_entry, self._api.refresh_token - ) - - # If we've reached this point using an emergency refresh token, we're in the - # clear and we can discard it: - if self._emergency_refresh_token_used: - self._emergency_refresh_token_used = False - class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ba51356f770..0faa07221aa 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) @@ -89,6 +89,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index ad973261a0e..23f85495025 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index b9e274666bb..331eb65ca83 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a048e4b0745..c2397e9f89e 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -33,7 +33,11 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -102,7 +106,11 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -120,6 +128,8 @@ async def test_step_reauth(hass): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -151,7 +161,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -197,7 +207,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } From 8901e1f15776c3ea5b6125394a1640008b0f8347 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 19 Jun 2021 00:10:05 +0000 Subject: [PATCH 433/750] [ci skip] Translation update --- .../components/ambee/translations/es.json | 14 +++++ .../ambee/translations/sensor.es.json | 10 ++++ .../components/bosch_shc/translations/es.json | 17 ++++++ .../demo/translations/select.es.json | 9 ++++ .../demo/translations/select.et.json | 9 ++++ .../demo/translations/select.ru.json | 9 ++++ .../demo/translations/select.zh-Hant.json | 9 ++++ .../components/goalzero/translations/es.json | 4 ++ .../homekit_controller/translations/es.json | 2 + .../translations/es.json | 1 + .../components/isy994/translations/es.json | 8 +++ .../components/kraken/translations/es.json | 12 +++++ .../modern_forms/translations/es.json | 15 ++++++ .../components/nexia/translations/es.json | 1 + .../pvpc_hourly_pricing/translations/es.json | 15 ++++++ .../pvpc_hourly_pricing/translations/ru.json | 21 ++++++-- .../components/samsungtv/translations/es.json | 4 ++ .../components/select/translations/es.json | 3 ++ .../components/select/translations/et.json | 3 ++ .../components/select/translations/ru.json | 3 ++ .../select/translations/zh-Hant.json | 8 +++ .../components/sia/translations/es.json | 8 +++ .../components/upnp/translations/es.json | 10 ++++ .../components/wemo/translations/es.json | 5 ++ .../components/wled/translations/es.json | 9 ++++ .../xiaomi_miio/translations/es.json | 52 ++++++++++++++++++- .../yamaha_musiccast/translations/es.json | 16 ++++++ .../components/zha/translations/es.json | 2 + 28 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/ambee/translations/es.json create mode 100644 homeassistant/components/ambee/translations/sensor.es.json create mode 100644 homeassistant/components/demo/translations/select.es.json create mode 100644 homeassistant/components/demo/translations/select.et.json create mode 100644 homeassistant/components/demo/translations/select.ru.json create mode 100644 homeassistant/components/demo/translations/select.zh-Hant.json create mode 100644 homeassistant/components/kraken/translations/es.json create mode 100644 homeassistant/components/modern_forms/translations/es.json create mode 100644 homeassistant/components/select/translations/es.json create mode 100644 homeassistant/components/select/translations/et.json create mode 100644 homeassistant/components/select/translations/ru.json create mode 100644 homeassistant/components/select/translations/zh-Hant.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/es.json diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json new file mode 100644 index 00000000000..de5ce971fa0 --- /dev/null +++ b/homeassistant/components/ambee/translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "description": "Vuelva a autenticarse con su cuenta de Ambee." + } + }, + "user": { + "description": "Configure Ambee para que se integre con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.es.json b/homeassistant/components/ambee/translations/sensor.es.json new file mode 100644 index 00000000000..a676ca7aa5e --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.es.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Alto", + "low": "Bajo", + "moderate": "Moderado", + "very high": "Muy alto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 71aaac2dfb2..2b3d4ca7479 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -1,7 +1,24 @@ { "config": { + "error": { + "pairing_failed": "El emparejamiento ha fallado; compruebe que el Bosch Smart Home Controller est\u00e1 en modo de emparejamiento (el LED parpadea) y que su contrase\u00f1a es correcta.", + "session_error": "Error de sesi\u00f3n: La API devuelve un resultado no correcto." + }, + "flow_title": "Bosch SHC: {name}", "step": { + "confirm_discovery": { + "description": "Pulse el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" + }, + "credentials": { + "data": { + "password": "Contrase\u00f1a del controlador smart home" + } + }, + "reauth_confirm": { + "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta" + }, "user": { + "description": "Configura tu Bosch Smart Home Controller para permitir la supervisi\u00f3n y el control con Home Assistant.", "title": "Par\u00e1metros de autenticaci\u00f3n SHC" } } diff --git a/homeassistant/components/demo/translations/select.es.json b/homeassistant/components/demo/translations/select.es.json new file mode 100644 index 00000000000..b2a20fdcfec --- /dev/null +++ b/homeassistant/components/demo/translations/select.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocidad de la luz", + "ludicrous_speed": "Velocidad rid\u00edcula", + "ridiculous_speed": "Velocidad rid\u00edcula" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.et.json b/homeassistant/components/demo/translations/select.et.json new file mode 100644 index 00000000000..eee34b646cc --- /dev/null +++ b/homeassistant/components/demo/translations/select.et.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Valguse kiirus", + "ludicrous_speed": "Meeletu kiirus", + "ridiculous_speed": "Naeruv\u00e4\u00e4rne kiirus" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.ru.json b/homeassistant/components/demo/translations/select.ru.json new file mode 100644 index 00000000000..0de63078eeb --- /dev/null +++ b/homeassistant/components/demo/translations/select.ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0441\u0432\u0435\u0442\u0430", + "ludicrous_speed": "\u0427\u0443\u0434\u043e\u0432\u0438\u0449\u043d\u0430\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c", + "ridiculous_speed": "\u041d\u0435\u0432\u0435\u0440\u043e\u044f\u0442\u043d\u0430\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.zh-Hant.json b/homeassistant/components/demo/translations/select.zh-Hant.json new file mode 100644 index 00000000000..6190837b1f1 --- /dev/null +++ b/homeassistant/components/demo/translations/select.zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "\u5149\u901f", + "ludicrous_speed": "\u53ef\u7b11\u7684\u901f\u5ea6", + "ridiculous_speed": "\u8352\u8b2c\u7684\u901f\u5ea6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index 4a2c7eeca62..06ee47fd1ca 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -9,6 +9,10 @@ "unknown": "Error inesperado" }, "step": { + "confirm_discovery": { + "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 9f5f40bd199..52b295ecf21 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", + "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado es inseguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.", "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, "description": "El controlador de HomeKit se comunica con {name} a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Introduce el c\u00f3digo de vinculaci\u00f3n de tu HomeKit (con el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index 1095ed06ed7..a5cf5303000 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -7,6 +7,7 @@ "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, + "flow_title": "{name} ( {host} )", "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 06824938c9d..46dc3260f83 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -36,5 +36,13 @@ "title": "Opciones ISY994" } } + }, + "system_health": { + "info": { + "device_connected": "ISY conectado", + "host_reachable": "Anfitri\u00f3n accesible", + "last_heartbeat": "Hora del \u00faltimo latido", + "websocket_status": "Estado del conector de eventos" + } } } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json new file mode 100644 index 00000000000..afcf3f92d45 --- /dev/null +++ b/homeassistant/components/kraken/translations/es.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de actualizaci\u00f3n", + "tracked_asset_pairs": "Pares de activos rastreados" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/es.json b/homeassistant/components/modern_forms/translations/es.json new file mode 100644 index 00000000000..ac911baf4a4 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Configura tu ventilador de Modern Forms para que se integre con Home Assistant." + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms llamado `{name}` a Home Assistant?", + "title": "Dispositivo de ventilador de Modern Forms descubierto" + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index d2d92d1e85b..1698b8db1d1 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marca", "password": "Contrase\u00f1a", "username": "Usuario" }, diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json index abc493b0790..59c6f6de174 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json @@ -7,11 +7,26 @@ "user": { "data": { "name": "Nombre del sensor", + "power": "Potencia contratada (kW)", + "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" }, "description": "Este sensor utiliza la API oficial para obtener [el precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visita los [documentos de la integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelecciona la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", "title": "Selecci\u00f3n de tarifa" } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Potencia contratada (kW)", + "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", + "tariff": "Tarifa aplicable por zona geogr\u00e1fica" + }, + "description": "Este sensor utiliza la API oficial para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara una explicaci\u00f3n m\u00e1s precisa visite los [documentos de integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configuraci\u00f3n del sensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json index e48e5222f3c..e68bc7e6289 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0438\u043b\u0438 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430)" + "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)", + "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)", + "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435" }, - "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0430\u0440\u0438\u0444, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u0430\u0441\u0447\u0435\u0442\u043d\u044b\u0445 \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0438\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: electric car (nightly rate of 3 periods)", - "title": "\u0412\u044b\u0431\u043e\u0440 \u0442\u0430\u0440\u0438\u0444\u0430" + "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)", + "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)", + "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435" + }, + "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" } } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index ceb37aaee1b..0228ca3101f 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -5,6 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", + "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." }, "flow_title": "Televisor Samsung: {model}", @@ -13,6 +14,9 @@ "description": "\u00bfQuieres configurar la televisi\u00f3n Samsung {model}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n. Cualquier configuraci\u00f3n manual de esta TV se sobreescribir\u00e1.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json new file mode 100644 index 00000000000..193c6b702c7 --- /dev/null +++ b/homeassistant/components/select/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Seleccionar" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/et.json b/homeassistant/components/select/translations/et.json new file mode 100644 index 00000000000..e747d2d4167 --- /dev/null +++ b/homeassistant/components/select/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Vali" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/ru.json b/homeassistant/components/select/translations/ru.json new file mode 100644 index 00000000000..9183e4c429d --- /dev/null +++ b/homeassistant/components/select/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/zh-Hant.json b/homeassistant/components/select/translations/zh-Hant.json new file mode 100644 index 00000000000..6f997c17528 --- /dev/null +++ b/homeassistant/components/select/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "current_option_changed": "{entity_name} \u9078\u9805\u5df2\u8b8a\u66f4" + } + }, + "title": "\u9078\u64c7" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json index 02d266869bf..f32b6a86626 100644 --- a/homeassistant/components/sia/translations/es.json +++ b/homeassistant/components/sia/translations/es.json @@ -1,5 +1,13 @@ { "config": { + "error": { + "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", + "invalid_account_length": "La cuenta no tiene la longitud adecuada, tiene que tener entre 3 y 16 caracteres.", + "invalid_key_format": "La clave no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", + "invalid_key_length": "La clave no tiene la longitud correcta, tiene que ser de 16, 24 o 32 caracteres hexadecimales.", + "invalid_ping": "El intervalo de ping debe estar entre 1 y 1440 minutos.", + "invalid_zones": "Tiene que haber al menos 1 zona." + }, "step": { "additional_account": { "title": "Agrega otra cuenta al puerto actual." diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 2165979a75d..356376e2e07 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)", + "unique_id": "Dispositivo", "usn": "Dispositivo" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/es.json b/homeassistant/components/wemo/translations/es.json index 1c7088a5280..4c176762d04 100644 --- a/homeassistant/components/wemo/translations/es.json +++ b/homeassistant/components/wemo/translations/es.json @@ -9,5 +9,10 @@ "description": "\u00bfQuieres configurar Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Se ha pulsado el bot\u00f3n Wemo durante 2 segundos" + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 77c324f46a1..c1c50986b61 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -20,5 +20,14 @@ "title": "Dispositivo WLED detectado" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Mantenga la luz principal, incluso con 1 segmento de LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index b5ec01007c0..0193cd4a39a 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -2,15 +2,37 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha." + "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha.", + "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", + "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio." }, "error": { "cannot_connect": "No se pudo conectar", + "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds", + "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xioami Miio Cloud, comprueba las credenciales.", + "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.", "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.", "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n." }, "flow_title": "Xiaomi Miio: {name}", "step": { + "cloud": { + "data": { + "cloud_country": "Pa\u00eds del servidor de la nube", + "cloud_password": "Contrase\u00f1a de la nube", + "cloud_username": "Nombre de usuario de la nube", + "manual": "Configurar manualmente (no recomendado)" + }, + "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar.", + "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" + }, + "connect": { + "data": { + "model": "Modelo del dispositivo" + }, + "description": "Seleccione manualmente el modelo de dispositivo entre los modelos admitidos.", + "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" + }, "device": { "data": { "host": "Direcci\u00f3n IP", @@ -30,6 +52,20 @@ "description": "Necesitar\u00e1s el token de la API de 32 caracteres, revisa https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para m\u00e1s instrucciones. Por favor, ten en cuenta que este token es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", "title": "Conectar con un Xiaomi Gateway" }, + "manual": { + "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", + "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan." + }, + "select": { + "data": { + "select_device": "Dispositivo Miio" + }, + "description": "Selecciona el dispositivo Xiaomi Miio para configurarlo.", + "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" + }, "user": { "data": { "gateway": "Conectar con un Xiaomi Gateway" @@ -38,5 +74,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Utilice la nube para conectar subdispositivos" + }, + "description": "Especifique los ajustes opcionales", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json new file mode 100644 index 00000000000..c63baa7b576 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n del ssdp." + }, + "error": { + "no_musiccast_device": "Este dispositivo no parece ser un dispositivo MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "user": { + "description": "Configura MusicCast para integrarse con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 5655d67fd34..4753834a493 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -41,6 +41,8 @@ "title": "Opciones del panel de control de la alarma" }, "zha_options": { + "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", + "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)", "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", "title": "Opciones globales" From c246e211eb416b7bea798c6d16f64e4a50f70671 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 19 Jun 2021 05:22:27 +0200 Subject: [PATCH 434/750] Update xknx to 0.18.7 (#52000) --- homeassistant/components/knx/climate.py | 12 ++++++++++++ homeassistant/components/knx/knx_entity.py | 8 +------- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 298ae391fe4..1a4b15c4265 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -324,3 +324,15 @@ class KNXClimate(KnxEntity, ClimateEntity): knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Store register state change callback.""" + super().async_added_to_hass() + if self._device.mode is not None: + self._device.mode.register_device_updated_cb(self.after_update_callback) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + super().async_will_remove_from_hass() + if self._device.mode is not None: + self._device.mode.unregister_device_updated_cb(self.after_update_callback) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 1e374250bba..a85fb2a561f 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import cast -from xknx.devices import Climate as XknxClimate, Device as XknxDevice +from xknx.devices import Device as XknxDevice from homeassistant.helpers.entity import Entity @@ -52,12 +52,6 @@ class KnxEntity(Entity): """Store register state change callback.""" self._device.register_device_updated_cb(self.after_update_callback) - if isinstance(self._device, XknxClimate) and self._device.mode is not None: - self._device.mode.register_device_updated_cb(self.after_update_callback) - async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) - - if isinstance(self._device, XknxClimate) and self._device.mode is not None: - self._device.mode.unregister_device_updated_cb(self.after_update_callback) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index e9a76a4ef57..8f46f904466 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.6"], + "requirements": ["xknx==0.18.7"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 136dad05b50..86625ff62f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2380,7 +2380,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.6 +xknx==0.18.7 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa4d61cc095..e67f854571e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1292,7 +1292,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.6 +xknx==0.18.7 # homeassistant.components.bluesound # homeassistant.components.rest From 1d941284ffc242423dbaeb64484643f63f688838 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 19 Jun 2021 08:10:17 +0200 Subject: [PATCH 435/750] Fix not awaiting async super method in KNX climate (#52005) --- homeassistant/components/knx/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 1a4b15c4265..adf585555fd 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -327,12 +327,12 @@ class KNXClimate(KnxEntity, ClimateEntity): async def async_added_to_hass(self) -> None: """Store register state change callback.""" - super().async_added_to_hass() + await super().async_added_to_hass() if self._device.mode is not None: self._device.mode.register_device_updated_cb(self.after_update_callback) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" - super().async_will_remove_from_hass() + await super().async_will_remove_from_hass() if self._device.mode is not None: self._device.mode.unregister_device_updated_cb(self.after_update_callback) From 34a44b9bec4c564d557a8b03a9f65a77554b8938 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 19 Jun 2021 13:25:26 +0200 Subject: [PATCH 436/750] Use entity sources to find related entities in Search (#51966) Co-authored-by: Martin Hjelmare --- homeassistant/components/search/__init__.py | 8 +++++ tests/components/search/test_init.py | 40 ++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index db97410469f..93da95bc550 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -8,6 +8,7 @@ from homeassistant.components import automation, group, script, websocket_api from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.entity import entity_sources as get_entity_sources DOMAIN = "search" _LOGGER = logging.getLogger(__name__) @@ -44,6 +45,7 @@ def websocket_search_related(hass, connection, msg): hass, device_registry.async_get(hass), entity_registry.async_get(hass), + get_entity_sources(hass), ) connection.send_result( msg["id"], searcher.async_search(msg["item_type"], msg["item_id"]) @@ -69,11 +71,13 @@ class Searcher: hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: entity_registry.EntityRegistry, + entity_sources: "dict[str, dict[str, str]]", ) -> None: """Search results.""" self.hass = hass self._device_reg = device_reg self._entity_reg = entity_reg + self._sources = entity_sources self.results = defaultdict(set) self._to_resolve = deque() @@ -184,6 +188,10 @@ class Searcher: if entity_entry.config_entry_id is not None: self._add_or_resolve("config_entry", entity_entry.config_entry_id) + else: + source = self._sources.get(entity_id) + if source is not None and "config_entry" in source: + self._add_or_resolve("config_entry", source["config_entry"]) domain = split_entity_id(entity_id)[0] diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index 82935f2b41f..e9d320aa9ef 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -3,6 +3,7 @@ from homeassistant.components import search from homeassistant.helpers import ( area_registry as ar, device_registry as dr, + entity, entity_registry as er, ) from homeassistant.setup import async_setup_component @@ -10,6 +11,18 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 +MOCK_ENTITY_SOURCES = { + "light.platform_config_source": { + "source": entity.SOURCE_PLATFORM_CONFIG, + "domain": "wled", + }, + "light.config_entry_source": { + "source": entity.SOURCE_CONFIG_ENTRY, + "config_entry": "config_entry_id", + "domain": "wled", + }, +} + async def test_search(hass): """Test that search works.""" @@ -48,6 +61,18 @@ async def test_search(hass): device_id=wled_device.id, ) + entity_sources = { + "light.wled_platform_config_source": { + "source": entity.SOURCE_PLATFORM_CONFIG, + "domain": "wled", + }, + "light.wled_config_entry_source": { + "source": entity.SOURCE_CONFIG_ENTRY, + "config_entry": wled_config_entry.entry_id, + "domain": "wled", + }, + } + # Non related info. kitchen_area = area_reg.async_create("Kitchen") @@ -221,7 +246,7 @@ async def test_search(hass): ("automation", "automation.wled_entity"), ("automation", "automation.wled_device"), ): - searcher = search.Searcher(hass, device_reg, entity_reg) + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) results = searcher.async_search(search_type, search_id) # Add the item we searched for, it's omitted from results results.setdefault(search_type, set()).add(search_id) @@ -254,7 +279,7 @@ async def test_search(hass): ("scene", "scene.scene_wled_hue"), ("group", "group.wled_hue"), ): - searcher = search.Searcher(hass, device_reg, entity_reg) + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) results = searcher.async_search(search_type, search_id) # Add the item we searched for, it's omitted from results results.setdefault(search_type, set()).add(search_id) @@ -276,9 +301,14 @@ async def test_search(hass): ("script", "script.non_existing"), ("automation", "automation.non_existing"), ): - searcher = search.Searcher(hass, device_reg, entity_reg) + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) assert searcher.async_search(search_type, search_id) == {} + searcher = search.Searcher(hass, device_reg, entity_reg, entity_sources) + assert searcher.async_search("entity", "light.wled_config_entry_source") == { + "config_entry": {wled_config_entry.entry_id}, + } + async def test_area_lookup(hass): """Test area based lookup.""" @@ -326,13 +356,13 @@ async def test_area_lookup(hass): }, ) - searcher = search.Searcher(hass, device_reg, entity_reg) + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) assert searcher.async_search("area", living_room_area.id) == { "script": {"script.wled"}, "automation": {"automation.area_turn_on"}, } - searcher = search.Searcher(hass, device_reg, entity_reg) + searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) assert searcher.async_search("automation", "automation.area_turn_on") == { "area": {living_room_area.id}, } From f550c318865d2f9a31953672ce242deddc0f5ec8 Mon Sep 17 00:00:00 2001 From: Oderik Date: Sat, 19 Jun 2021 15:38:48 +0200 Subject: [PATCH 437/750] Fix IoT class (#52008) --- homeassistant/components/min_max/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index 525d6c0ac1a..cf8c78d46ac 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/min_max", "codeowners": ["@fabaff"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_push" } From 24c1256c2c8bef2d39a2e483ac09689d850181a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Jun 2021 17:13:48 +0200 Subject: [PATCH 438/750] Small WLED cleanups (#52014) --- homeassistant/components/wled/light.py | 65 ++++++++++---------------- tests/components/wled/test_light.py | 19 ++++++-- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index c9a074bbdce..46dba23b39e 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -132,27 +132,24 @@ class WLEDMasterLight(WLEDEntity, LightEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data: dict[str, bool | int] = {ATTR_ON: False} - + transition = None if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. - data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + transition = round(kwargs[ATTR_TRANSITION] * 10) - await self.coordinator.wled.master(**data) # type: ignore[arg-type] + await self.coordinator.wled.master(on=False, transition=transition) @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data: dict[str, bool | int] = {ATTR_ON: True} - + transition = None if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. - data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + transition = round(kwargs[ATTR_TRANSITION] * 10) - if ATTR_BRIGHTNESS in kwargs: - data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] - - await self.coordinator.wled.master(**data) # type: ignore[arg-type] + await self.coordinator.wled.master( + on=True, brightness=kwargs.get(ATTR_BRIGHTNESS), transition=transition + ) async def async_effect( self, @@ -171,9 +168,7 @@ class WLEDMasterLight(WLEDEntity, LightEntity): preset: int, ) -> None: """Set a WLED light to a saved preset.""" - data = {ATTR_PRESET: preset} - - await self.coordinator.wled.preset(**data) + await self.coordinator.wled.preset(preset=preset) class WLEDSegmentLight(WLEDEntity, LightEntity): @@ -292,22 +287,22 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data: dict[str, bool | int] = {ATTR_ON: False} - + transition = None if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. - data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + transition = round(kwargs[ATTR_TRANSITION] * 10) # If there is a single segment, control via the master if ( not self._keep_master_light and len(self.coordinator.data.state.segments) == 1 ): - await self.coordinator.wled.master(**data) # type: ignore[arg-type] + await self.coordinator.wled.master(on=False, transition=transition) return - data[ATTR_SEGMENT_ID] = self._segment - await self.coordinator.wled.segment(**data) # type: ignore[arg-type] + await self.coordinator.wled.segment( + segment_id=self._segment, on=False, transition=transition + ) @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: @@ -364,24 +359,14 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): speed: int | None = None, ) -> None: """Set the effect of a WLED light.""" - data: dict[str, bool | int | str | None] = {ATTR_SEGMENT_ID: self._segment} - - if effect is not None: - data[ATTR_EFFECT] = effect - - if intensity is not None: - data[ATTR_INTENSITY] = intensity - - if palette is not None: - data[ATTR_PALETTE] = palette - - if reverse is not None: - data[ATTR_REVERSE] = reverse - - if speed is not None: - data[ATTR_SPEED] = speed - - await self.coordinator.wled.segment(**data) # type: ignore[arg-type] + await self.coordinator.wled.segment( + segment_id=self._segment, + effect=effect, + intensity=intensity, + palette=palette, + reverse=reverse, + speed=speed, + ) @wled_exception_handler async def async_preset( @@ -389,9 +374,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): preset: int, ) -> None: """Set a WLED light to a saved preset.""" - data = {ATTR_PRESET: preset} - - await self.coordinator.wled.preset(**data) + await self.coordinator.wled.preset(preset=preset) @callback diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 3ce0e167cb5..a4e3f712547 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -330,7 +330,7 @@ async def test_light_error( assert state.state == STATE_ON assert "Invalid response from API" in caplog.text assert mock_wled.segment.call_count == 1 - mock_wled.segment.assert_called_with(on=False, segment_id=0) + mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None) async def test_light_connection_error( @@ -355,7 +355,7 @@ async def test_light_connection_error( assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text assert mock_wled.segment.call_count == 1 - mock_wled.segment.assert_called_with(on=False, segment_id=0) + mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None) @pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) @@ -425,6 +425,10 @@ async def test_effect_service( mock_wled.segment.assert_called_with( segment_id=0, effect=9, + intensity=None, + palette=None, + reverse=None, + speed=None, ) await hass.services.async_call( @@ -445,6 +449,8 @@ async def test_effect_service( reverse=True, segment_id=0, speed=100, + effect=None, + palette=None, ) await hass.services.async_call( @@ -467,6 +473,7 @@ async def test_effect_service( reverse=True, segment_id=0, speed=100, + intensity=None, ) await hass.services.async_call( @@ -487,6 +494,8 @@ async def test_effect_service( intensity=200, segment_id=0, speed=100, + palette=None, + reverse=None, ) await hass.services.async_call( @@ -507,6 +516,8 @@ async def test_effect_service( intensity=200, reverse=True, segment_id=0, + palette=None, + speed=None, ) @@ -532,7 +543,9 @@ async def test_effect_service_error( assert state.state == STATE_ON assert "Invalid response from API" in caplog.text assert mock_wled.segment.call_count == 1 - mock_wled.segment.assert_called_with(effect=9, segment_id=0) + mock_wled.segment.assert_called_with( + effect=9, segment_id=0, intensity=None, palette=None, reverse=None, speed=None + ) async def test_preset_service( From 3836d46dffea2a7cb3e05aa540a20c69ffef690e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 20 Jun 2021 00:10:44 +0000 Subject: [PATCH 439/750] [ci skip] Translation update --- .../components/demo/translations/select.ca.json | 9 +++++++++ .../components/select/translations/ca.json | 14 ++++++++++++++ .../components/select/translations/et.json | 11 +++++++++++ .../components/select/translations/ru.json | 11 +++++++++++ .../components/select/translations/zh-Hant.json | 6 ++++++ .../components/simplisafe/translations/ca.json | 2 +- .../components/simplisafe/translations/et.json | 2 +- .../components/simplisafe/translations/ru.json | 2 +- .../simplisafe/translations/zh-Hant.json | 2 +- 9 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.ca.json create mode 100644 homeassistant/components/select/translations/ca.json diff --git a/homeassistant/components/demo/translations/select.ca.json b/homeassistant/components/demo/translations/select.ca.json new file mode 100644 index 00000000000..c66c285ffda --- /dev/null +++ b/homeassistant/components/demo/translations/select.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocitat de la llum", + "ludicrous_speed": "Velocitat Ludicrous", + "ridiculous_speed": "Velocitat rid\u00edcula" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/ca.json b/homeassistant/components/select/translations/ca.json new file mode 100644 index 00000000000..bcd9b348cbb --- /dev/null +++ b/homeassistant/components/select/translations/ca.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Canvia l'opci\u00f3 de {entity_name}" + }, + "condition_type": { + "selected_option": "Opci\u00f3 actual seleccionada de {entity_name}" + }, + "trigger_type": { + "current_option_changed": "{entity_name} canvi\u00ef d'opci\u00f3" + } + }, + "title": "Selector" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/et.json b/homeassistant/components/select/translations/et.json index e747d2d4167..7fff5de9f5c 100644 --- a/homeassistant/components/select/translations/et.json +++ b/homeassistant/components/select/translations/et.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "Muuda {entity_name} s\u00e4tteid" + }, + "condition_type": { + "selected_option": "Praegused {entity_name} s\u00e4tted" + }, + "trigger_type": { + "current_option_changed": "Olemi {entity_name} s\u00e4tted on muudetud" + } + }, "title": "Vali" } \ No newline at end of file diff --git a/homeassistant/components/select/translations/ru.json b/homeassistant/components/select/translations/ru.json index 9183e4c429d..5bbdd279b43 100644 --- a/homeassistant/components/select/translations/ru.json +++ b/homeassistant/components/select/translations/ru.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430 {entity_name}" + }, + "condition_type": { + "selected_option": "{entity_name} \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u043c" + }, + "trigger_type": { + "current_option_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430" + } + }, "title": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c" } \ No newline at end of file diff --git a/homeassistant/components/select/translations/zh-Hant.json b/homeassistant/components/select/translations/zh-Hant.json index 6f997c17528..2bf3114508a 100644 --- a/homeassistant/components/select/translations/zh-Hant.json +++ b/homeassistant/components/select/translations/zh-Hant.json @@ -1,5 +1,11 @@ { "device_automation": { + "action_type": { + "select_option": "\u8b8a\u66f4{entity_name}\u9078\u9805" + }, + "condition_type": { + "selected_option": "\u76ee\u524d{entity_name}\u5df2\u9078\u9078\u9805" + }, "trigger_type": { "current_option_changed": "{entity_name} \u9078\u9805\u5df2\u8b8a\u66f4" } diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 5b5d0cf1798..e8bb80d1b88 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -19,7 +19,7 @@ "data": { "password": "Contrasenya" }, - "description": "El token d'acc\u00e9s ha caducat o ha estat revocat. Introdueix la teva contrasenya per tornar a vincular el compte.", + "description": "L'acc\u00e9s ha caducat o ha estat revocat. Introdueix la teva contrasenya per tornar a vincular el compte.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 7b6e317b922..e815785f0b5 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -19,7 +19,7 @@ "data": { "password": "Salas\u00f5na" }, - "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i see on t\u00fchistatud. Konto uuesti linkimiseks sisesta oma parool.", + "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto uuesti linkimiseks sisesta oma salas\u00f5na.", "title": "Taastuvasta SimpliSafe'i konto" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index bcfffc57533..5fc3fce065e 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -19,7 +19,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", + "description": "\u0421\u0440\u043e\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0431\u044b\u043b \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 27064ed1055..517d48321a8 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -19,7 +19,7 @@ "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5b58\u53d6\u6b0a\u6756\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", + "description": "\u5b58\u53d6\u6b0a\u9650\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { From e8b5790846480d73aa4ce6adbf88546fe5e5931e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:38:02 +0800 Subject: [PATCH 440/750] Clean up stream refactor (#51951) * Clean up target_duration method * Consolidate Part creation in one place * Use BytesIO.read instead of memoryview access * Change flush() signature --- homeassistant/components/stream/core.py | 18 +++--- homeassistant/components/stream/fmp4utils.py | 2 +- homeassistant/components/stream/hls.py | 5 +- homeassistant/components/stream/worker.py | 66 +++++++++----------- tests/components/stream/test_hls.py | 1 - 5 files changed, 44 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 5f8bb736761..d840bfaf858 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -14,7 +14,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ATTR_STREAMS, DOMAIN, TARGET_SEGMENT_DURATION if TYPE_CHECKING: from . import Stream @@ -28,6 +28,7 @@ class Part: duration: float = attr.ib() has_keyframe: bool = attr.ib() + # video data (moof+mdat) data: bytes = attr.ib() @@ -50,7 +51,7 @@ class Segment: return self.duration > 0 def get_bytes_without_init(self) -> bytes: - """Return reconstructed data for entire segment as bytes.""" + """Return reconstructed data for all parts as bytes, without init.""" return b"".join([part.data for part in self.parts]) @@ -141,17 +142,16 @@ class StreamOutput: return None @property - def target_duration(self) -> int: + def target_duration(self) -> float: """Return the max duration of any given segment in seconds.""" - segment_length = len(self._segments) - if not segment_length: - return 1 - durations = [s.duration for s in self._segments] - return round(max(durations)) or 1 + if not (durations := [s.duration for s in self._segments if s.complete]): + return TARGET_SEGMENT_DURATION + return max(durations) def get_segment(self, sequence: int) -> Segment | None: """Retrieve a specific segment.""" - for segment in self._segments: + # Most hits will come in the most recent segments, so iterate reversed + for segment in reversed(self._segments): if segment.sequence == sequence: return segment return None diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index ef01158be62..f136784cf87 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -5,7 +5,7 @@ from collections.abc import Generator def find_box( - mp4_bytes: bytes | memoryview, target_type: bytes, box_start: int = 0 + mp4_bytes: bytes, target_type: bytes, box_start: int = 0 ) -> Generator[int, None, None]: """Find location of first box (or sub_box if box_start provided) of given type.""" if box_start == 0: diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index d7167e0b7de..7f11bc09655 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -173,8 +173,9 @@ class HlsInitView(StreamView): track = stream.add_provider(HLS_PROVIDER) if not (segments := track.get_segments()): return web.HTTPNotFound() - headers = {"Content-Type": "video/mp4"} - return web.Response(body=segments[0].init, headers=headers) + return web.Response( + body=segments[0].init, headers={"Content-Type": "video/mp4"} + ) class HlsSegmentView(StreamView): diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 3023b8cd85c..04be79e668e 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections import deque from collections.abc import Iterator, Mapping -from fractions import Fraction from io import BytesIO import logging from threading import Event @@ -49,7 +48,8 @@ class SegmentBuffer: self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: Any | None = None # av.audio.AudioStream | None self._segment: Segment | None = None - self._segment_last_write_pos: int = cast(int, None) + # the following 3 member variables are used for Part formation + self._memory_file_pos: int = cast(int, None) self._part_start_dts: int = cast(int, None) self._part_has_keyframe = False @@ -93,10 +93,10 @@ class SegmentBuffer: """Initialize a new stream segment.""" # Keep track of the number of segments we've processed self._sequence += 1 - self._segment_start_dts = self._part_start_dts = video_dts + self._segment_start_dts = video_dts self._segment = None - self._segment_last_write_pos = 0 self._memory_file = BytesIO() + self._memory_file_pos = 0 self._av_output = self.make_new_av( memory_file=self._memory_file, sequence=self._sequence, @@ -120,14 +120,11 @@ class SegmentBuffer: if ( packet.is_keyframe - and ( - segment_duration := (packet.dts - self._segment_start_dts) - * packet.time_base - ) + and (packet.dts - self._segment_start_dts) * packet.time_base >= MIN_SEGMENT_DURATION ): # Flush segment (also flushes the stub part segment) - self.flush(segment_duration, packet) + self.flush(packet, last_part=True) # Reinitialize self.reset(packet.dts) @@ -143,8 +140,7 @@ class SegmentBuffer: def check_flush_part(self, packet: av.Packet) -> None: """Check for and mark a part segment boundary and record its duration.""" - byte_position = self._memory_file.tell() - if self._segment_last_write_pos == byte_position: + if self._memory_file_pos == self._memory_file.tell(): return if self._segment is None: # We have our first non-zero byte position. This means the init has just @@ -154,43 +150,43 @@ class SegmentBuffer: stream_id=self._stream_id, init=self._memory_file.getvalue(), ) - self._segment_last_write_pos = byte_position + self._memory_file_pos = self._memory_file.tell() + self._part_start_dts = self._segment_start_dts # Fetch the latest StreamOutputs, which may have changed since the # worker started. for stream_output in self._outputs_callback().values(): stream_output.put(self._segment) else: # These are the ends of the part segments - self._segment.parts.append( - Part( - duration=float( - (packet.dts - self._part_start_dts) * packet.time_base - ), - has_keyframe=self._part_has_keyframe, - data=self._memory_file.getbuffer()[ - self._segment_last_write_pos : byte_position - ].tobytes(), - ) - ) - self._segment_last_write_pos = byte_position - self._part_start_dts = packet.dts - self._part_has_keyframe = False + self.flush(packet, last_part=False) - def flush(self, duration: Fraction, packet: av.Packet) -> None: - """Create a segment from the buffered packets and write to output.""" - self._av_output.close() + def flush(self, packet: av.Packet, last_part: bool) -> None: + """Output a part from the most recent bytes in the memory_file. + + If last_part is True, also close the segment, give it a duration, + and clean up the av_output and memory_file. + """ + if last_part: + # Closing the av_output will write the remaining buffered data to the + # memory_file as a new moof/mdat. + self._av_output.close() assert self._segment - self._segment.duration = float(duration) - # Also flush the part segment (need to close the output above before this) + self._memory_file.seek(self._memory_file_pos) self._segment.parts.append( Part( duration=float((packet.dts - self._part_start_dts) * packet.time_base), has_keyframe=self._part_has_keyframe, - data=self._memory_file.getbuffer()[ - self._segment_last_write_pos : - ].tobytes(), + data=self._memory_file.read(), ) ) - self._memory_file.close() # We don't need the BytesIO object anymore + if last_part: + self._segment.duration = float( + (packet.dts - self._segment_start_dts) * packet.time_base + ) + self._memory_file.close() # We don't need the BytesIO object anymore + else: + self._memory_file_pos = self._memory_file.tell() + self._part_start_dts = packet.dts + self._part_has_keyframe = False def discontinuity(self) -> None: """Mark the stream as having been restarted.""" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 89c07083b17..919f71c8509 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -264,7 +264,6 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) - for i in range(2): segment = Segment(sequence=i, duration=SEGMENT_DURATION, start_time=FAKE_TIME) hls.put(segment) From a127131c1b9180f2b2958ffe1c5dc2c4f641767e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 20 Jun 2021 18:17:39 +0200 Subject: [PATCH 441/750] Upgrade async_upnp_client to 0.19.0 (#52019) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 434ff0e9c39..87730aa1316 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.18.0"], + "requirements": ["async-upnp-client==0.19.0"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 59c992cd34d..faadfac5c0c 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "defusedxml==0.7.1", - "async-upnp-client==0.18.0" + "async-upnp-client==0.19.0" ], "dependencies": ["network"], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 5acd260ec9a..b252f5082cb 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.18.0"], + "requirements": ["async-upnp-client==0.19.0"], "dependencies": ["ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8e611e05502..a5a2288f202 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.18.0 +async-upnp-client==0.19.0 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 86625ff62f9..bfd344407d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -304,7 +304,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.18.0 +async-upnp-client==0.19.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e67f854571e..4c17635f8bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -196,7 +196,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.18.0 +async-upnp-client==0.19.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 57106098f9e729db5fe3c75f85f962c2cf7221f4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 20 Jun 2021 23:53:08 +0200 Subject: [PATCH 442/750] Fix AccuWeather sensors updates (#52031) Co-authored-by: Paulus Schoutsen --- .../components/accuweather/sensor.py | 30 +++++++++++++---- tests/components/accuweather/test_sensor.py | 33 +++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 09e9cda30ad..d6f9339409f 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_NAME, DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -81,16 +81,11 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) + self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind) if forecast_day is None: self._description = SENSOR_TYPES[kind] - self._sensor_data: dict[str, Any] - if kind == "Precipitation": - self._sensor_data = coordinator.data["PrecipitationSummary"][kind] - else: - self._sensor_data = coordinator.data[kind] else: self._description = FORECAST_SENSOR_TYPES[kind] - self._sensor_data = coordinator.data[ATTR_FORECAST][forecast_day][kind] self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL self._name = name self.kind = kind @@ -182,3 +177,24 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._description[ATTR_ENABLED] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle data update.""" + self._sensor_data = _get_sensor_data( + self.coordinator.data, self.forecast_day, self.kind + ) + self.async_write_ha_state() + + +def _get_sensor_data( + sensors: dict[str, Any], forecast_day: int | None, kind: str +) -> Any: + """Get sensor data.""" + if forecast_day is not None: + return sensors[ATTR_FORECAST][forecast_day][kind] + + if kind == "Precipitation": + return sensors["PrecipitationSummary"][kind] + + return sensors[kind] diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 482fae696c0..64c49c61fe7 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -673,3 +673,36 @@ async def test_sensor_imperial_units(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET + + +async def test_state_update(hass): + """Ensure the sensor state changes after updating the data.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3200" + + future = utcnow() + timedelta(minutes=60) + + current_condition = json.loads( + load_fixture("accuweather/current_conditions_data.json") + ) + current_condition["Ceiling"]["Metric"]["Value"] = 3300 + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current_condition, + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3300" From af8ef634c1bb6d31943361eb4cf907b6e046f0b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Jun 2021 14:53:21 -0700 Subject: [PATCH 443/750] Fix double subscriptions for local push notifications (#52039) --- .../components/mobile_app/__init__.py | 2 +- tests/components/mobile_app/test_notify.py | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 951c6f3beaf..9633ec6556d 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -160,7 +160,7 @@ def handle_push_notification_channel(hass, connection, msg): registered_channels = hass.data[DOMAIN][DATA_PUSH_CHANNEL] if webhook_id in registered_channels: - registered_channels.pop(webhook_id)() + registered_channels.pop(webhook_id) @callback def forward_push_notification(data): diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 9c4ca146898..1e3b999d5f5 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -136,6 +136,18 @@ async def test_notify_ws_works( sub_result = await client.receive_json() assert sub_result["success"] + # Subscribe twice, it should forward all messages to 2nd subscription + await client.send_json( + { + "id": 6, + "type": "mobile_app/push_notification_channel", + "webhook_id": "mock-webhook_id", + } + ) + + sub_result = await client.receive_json() + assert sub_result["success"] + assert await hass.services.async_call( "notify", "mobile_app_test", {"message": "Hello world"}, blocking=True ) @@ -144,13 +156,14 @@ async def test_notify_ws_works( msg_result = await client.receive_json() assert msg_result["event"] == {"message": "Hello world"} + assert msg_result["id"] == 6 # This is the new subscription # Unsubscribe, now it should go over http await client.send_json( { - "id": 6, + "id": 7, "type": "unsubscribe_events", - "subscription": 5, + "subscription": 6, } ) sub_result = await client.receive_json() @@ -165,7 +178,7 @@ async def test_notify_ws_works( # Test non-existing webhook ID await client.send_json( { - "id": 7, + "id": 8, "type": "mobile_app/push_notification_channel", "webhook_id": "non-existing", } @@ -180,7 +193,7 @@ async def test_notify_ws_works( # Test webhook ID linked to other user await client.send_json( { - "id": 8, + "id": 9, "type": "mobile_app/push_notification_channel", "webhook_id": "webhook_id_2", } From 067b5258c6bad7d020d3414449ec7aaffbd5be8a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 21 Jun 2021 00:09:58 +0000 Subject: [PATCH 444/750] [ci skip] Translation update --- .../components/ambee/translations/de.json | 6 +- .../demo/translations/select.de.json | 9 +++ .../pvpc_hourly_pricing/translations/de.json | 15 +++++ .../components/select/translations/de.json | 14 +++++ .../components/wemo/translations/de.json | 5 ++ .../components/wled/translations/de.json | 9 +++ .../xiaomi_miio/translations/de.json | 58 ++++++++++++++++++- .../yamaha_musiccast/translations/de.json | 23 ++++++++ 8 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.de.json create mode 100644 homeassistant/components/select/translations/de.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/de.json diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index cbe6dc96303..4359ab72349 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -10,7 +10,8 @@ "step": { "reauth_confirm": { "data": { - "api_key": "API-Schl\u00fcssel" + "api_key": "API-Schl\u00fcssel", + "description": "Authentifiziere dich erneut mit deinem Ambee-Konto." } }, "user": { @@ -19,7 +20,8 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" - } + }, + "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } } diff --git a/homeassistant/components/demo/translations/select.de.json b/homeassistant/components/demo/translations/select.de.json new file mode 100644 index 00000000000..2d801f47c13 --- /dev/null +++ b/homeassistant/components/demo/translations/select.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Lichtgeschwindigkeit", + "ludicrous_speed": "Wahnsinnige Geschwindigkeit", + "ridiculous_speed": "L\u00e4cherliche Geschwindigkeit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 1b5c4d37658..0af08c31961 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -7,11 +7,26 @@ "user": { "data": { "name": "Sensorname", + "power": "Vertraglich vereinbarte Leistung (kW)", + "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Vertragstarif (1, 2 oder 3 Perioden)" }, "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. \nWeitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nW\u00e4hlen Sie den vertraglich vereinbarten Tarif basierend auf der Anzahl der Abrechnungsperioden pro Tag aus: \n - 1 Periode: Normal \n - 2 Perioden: Diskriminierung (Nachttarif) \n - 3 Perioden: Elektroauto (Nachttarif von 3 Perioden)", "title": "Tarifauswahl" } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Vertraglich vereinbarte Leistung (kW)", + "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", + "tariff": "Geltender Tarif nach geografischer Zone" + }, + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten.\nEine genauere Erkl\u00e4rung finden Sie in den [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Sensoreinrichtung" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/select/translations/de.json b/homeassistant/components/select/translations/de.json new file mode 100644 index 00000000000..a54098b11ac --- /dev/null +++ b/homeassistant/components/select/translations/de.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Option {entity_name} \u00e4ndern" + }, + "condition_type": { + "selected_option": "Aktuelle {entity_name} ausgew\u00e4hlte Option" + }, + "trigger_type": { + "current_option_changed": "Option {entity_name} ge\u00e4ndert" + } + }, + "title": "Ausw\u00e4hlen" +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json index 81694f65ea2..b0735db1249 100644 --- a/homeassistant/components/wemo/translations/de.json +++ b/homeassistant/components/wemo/translations/de.json @@ -9,5 +9,10 @@ "description": "M\u00f6chtest du Wemo einrichten?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo-Taste wurde 2 Sekunden lang gedr\u00fcckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index d700e79e0b6..d03ef92d041 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -20,5 +20,14 @@ "title": "WLED-Ger\u00e4t entdeckt" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Master-Licht beibehalten, auch mit 1 LED-Segment." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index ef5c6c35a37..7f541180a55 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -2,15 +2,38 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "incomplete_info": "Unvollst\u00e4ndige Informationen zur Einrichtung des Ger\u00e4ts, kein Host oder Token geliefert.", + "not_xiaomi_miio": "Ger\u00e4t wird (noch) nicht von Xiaomi Miio unterst\u00fctzt.", + "reauth_successful": "[%key::common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben", + "cloud_login_error": "Konnte sich nicht bei Xioami Miio Cloud anmelden, \u00fcberpr\u00fcfe die Anmeldedaten.", + "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." }, "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Land des Cloud-Servers", + "cloud_password": "Cloud-Passwort", + "cloud_username": "Cloud-Benutzername", + "manual": "Manuell konfigurieren (nicht empfohlen)" + }, + "description": "Melde dich bei der Xiaomi Miio Cloud an, siehe https://www.openhab.org/addons/bindings/miio/#country-servers f\u00fcr den zu verwendenden Cloud-Server.", + "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Ger\u00e4temodell" + }, + "description": "W\u00e4hle das Ger\u00e4temodell manuell aus den unterst\u00fctzten Modellen aus.", + "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + }, "device": { "data": { "host": "IP-Adresse", @@ -30,6 +53,25 @@ "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, + "manual": { + "data": { + "host": "[%key::common::config_flow::data::ip%]", + "token": "API-Token" + }, + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr Anweisungen. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara-Integration verwendet wird.", + "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "Die Xiaomi Miio-Integration muss dein Konto neu authentifizieren, um die Token zu aktualisieren oder fehlende Cloud-Anmeldedaten hinzuzuf\u00fcgen.", + "title": "[%key::common::config_flow::title::reauth%]" + }, + "select": { + "data": { + "select_device": "Miio-Ger\u00e4t" + }, + "description": "W\u00e4hle das einzurichtende Xiaomi Miio-Ger\u00e4t aus.", + "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + }, "user": { "data": { "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" @@ -38,5 +80,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Cloud verwenden, um verbundene Subdevices zu erhalten" + }, + "description": "Optionale Einstellungen angeben", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json new file mode 100644 index 00000000000..49e66419bdf --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "[%key::common::config_flow::abort::already_configured_device%]", + "yxc_control_url_missing": "Die Steuer-URL ist in der ssdp-Beschreibung nicht angegeben." + }, + "error": { + "no_musiccast_device": "Dieses Ger\u00e4t scheint kein MusicCast-Ger\u00e4t zu sein." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "[%key::common::config_flow::description::confirm_setup%]" + }, + "user": { + "data": { + "host": "[%key::common::config_flow::data::host%]" + }, + "description": "Einrichten von MusicCast zur Integration mit Home Assistant." + } + } + } +} \ No newline at end of file From 7b5ed8faa8a6a0198dcd6452a51b1cbc816a473d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 20 Jun 2021 23:38:07 -0500 Subject: [PATCH 445/750] Catch unexpected battery update payloads on Sonos (#52040) --- homeassistant/components/sonos/speaker.py | 8 ++++++++ tests/components/sonos/test_sensor.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b010fa623be..ec59d946c09 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,6 +407,14 @@ class SonosSpeaker: """Update device properties from an event.""" if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) + if "BattChg" not in battery_dict: + _LOGGER.debug( + "Unknown device properties update for %s (%s), please report an issue: '%s'", + self.zone_name, + self.model_name, + more_info, + ) + return await self.async_update_battery_info(battery_dict) self.async_write_entity_states() diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 12c12821a0d..8d402b589b0 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -83,3 +83,23 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" + + +async def test_device_payload_without_battery( + hass, config_entry, config, soco, battery_event, caplog +): + """Test device properties event update without battery info.""" + soco.get_battery_info.return_value = None + + await setup_platform(hass, config_entry, config) + + subscription = soco.deviceProperties.subscribe.return_value + sub_callback = subscription.callback + + bad_payload = "BadKey:BadValue" + battery_event.variables["more_info"] = bad_payload + + sub_callback(battery_event) + await hass.async_block_till_done() + + assert bad_payload in caplog.text From d772488472c008ba2e671683fda9402c491786b6 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 20 Jun 2021 23:38:45 -0500 Subject: [PATCH 446/750] Remove undo listener variable in sonarr (#52042) --- homeassistant/components/sonarr/__init__.py | 6 +----- homeassistant/components/sonarr/const.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 08fc2c7f26d..f0969d063c1 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -23,7 +23,6 @@ from .const import ( CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DATA_SONARR, - DATA_UNDO_UPDATE_LISTENER, DEFAULT_UPCOMING_DAYS, DEFAULT_WANTED_MAX_ITEMS, DOMAIN, @@ -66,12 +65,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SonarrError as err: raise ConfigEntryNotReady from err - undo_listener = entry.add_update_listener(_async_update_listener) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_SONARR: sonarr, - DATA_UNDO_UPDATE_LISTENER: undo_listener, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -83,8 +81,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() - if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py index 52079a9416c..45b26166c92 100644 --- a/homeassistant/components/sonarr/const.py +++ b/homeassistant/components/sonarr/const.py @@ -17,7 +17,6 @@ CONF_WANTED_MAX_ITEMS = "wanted_max_items" # Data DATA_SONARR = "sonarr" -DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" # Defaults DEFAULT_BASE_PATH = "/api" From 01a26f134850ca3002e47d9e7f9d03bd818f3027 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 21 Jun 2021 06:39:14 +0200 Subject: [PATCH 447/750] Remove undo_listener variable in Sony Bravia TV integration (#52033) --- homeassistant/components/braviatv/__init__.py | 18 +++--------------- .../components/braviatv/config_flow.py | 5 +---- homeassistant/components/braviatv/const.py | 2 -- .../components/braviatv/media_player.py | 10 ++-------- homeassistant/components/braviatv/remote.py | 4 ++-- 5 files changed, 8 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 17e02f2c8f0..a29d899cb30 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -12,14 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ( - BRAVIA_COORDINATOR, - CLIENTID_PREFIX, - CONF_IGNORED_SOURCES, - DOMAIN, - NICKNAME, - UNDO_UPDATE_LISTENER, -) +from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME _LOGGER = logging.getLogger(__name__) @@ -35,15 +28,12 @@ async def async_setup_entry(hass, config_entry): ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources) - undo_listener = config_entry.add_update_listener(update_listener) + config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = { - BRAVIA_COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: undo_listener, - } + hass.data[DOMAIN][config_entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -56,8 +46,6 @@ async def async_unload_entry(hass, config_entry): config_entry, PLATFORMS ) - hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index e042c705b98..588014ebd3c 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -16,7 +16,6 @@ from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, - BRAVIA_COORDINATOR, CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, @@ -160,9 +159,7 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][ - BRAVIA_COORDINATOR - ] + coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] self.braviarc = coordinator.braviarc connected = await self.hass.async_add_executor_job(self.braviarc.is_connected) if not connected: diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index bc06b7c858a..1fa96e6a98d 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -6,10 +6,8 @@ ATTR_MODEL = "model" CONF_IGNORED_SOURCES = "ignored_sources" -BRAVIA_COORDINATOR = "bravia_coordinator" BRAVIA_CONFIG_FILE = "bravia.conf" CLIENTID_PREFIX = "HomeAssistant" DEFAULT_NAME = f"{ATTR_MANUFACTURER} Bravia TV" DOMAIN = "braviatv" NICKNAME = "Home Assistant" -UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index bb4ad32ed9b..7009b04cba7 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -34,13 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.json import load_json -from .const import ( - ATTR_MANUFACTURER, - BRAVIA_CONFIG_FILE, - BRAVIA_COORDINATOR, - DEFAULT_NAME, - DOMAIN, -) +from .const import ATTR_MANUFACTURER, BRAVIA_CONFIG_FILE, DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -99,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Bravia TV Media Player from a config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][BRAVIA_COORDINATOR] + coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id device_info = { "identifiers": {(DOMAIN, unique_id)}, diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index ca36276a88c..8bd5fb09af3 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -3,13 +3,13 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_MANUFACTURER, BRAVIA_COORDINATOR, DEFAULT_NAME, DOMAIN +from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Bravia TV Remote from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][BRAVIA_COORDINATOR] + coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id device_info = { "identifiers": {(DOMAIN, unique_id)}, From 2aed268fb78be8c0a1957ad798cf1a392fd7c482 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 21 Jun 2021 06:40:04 +0200 Subject: [PATCH 448/750] Remove `undo_listener` variable in AccuWeather integration (#52032) --- .../components/accuweather/__init__.py | 17 +++-------------- homeassistant/components/accuweather/const.py | 2 -- homeassistant/components/accuweather/sensor.py | 5 +---- .../components/accuweather/system_health.py | 6 +++--- homeassistant/components/accuweather/weather.py | 5 +---- .../accuweather/test_system_health.py | 10 +++------- 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 18a4bd2dce4..a2b428cf597 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -16,13 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - ATTR_FORECAST, - CONF_FORECAST, - COORDINATOR, - DOMAIN, - UNDO_UPDATE_LISTENER, -) +from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -45,12 +39,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - undo_listener = entry.add_update_listener(update_listener) + entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: undo_listener, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -61,8 +52,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 54d9b631ade..e834feae8d2 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -48,12 +48,10 @@ ATTR_LABEL: Final = "label" ATTR_UNIT_IMPERIAL: Final = "unit_imperial" ATTR_UNIT_METRIC: Final = "unit_metric" CONF_FORECAST: Final = "forecast" -COORDINATOR: Final = "coordinator" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 NAME: Final = "AccuWeather" -UNDO_UPDATE_LISTENER: Final = "undo_update_listener" CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index d6f9339409f..bf6b0efd6c2 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -28,7 +28,6 @@ from .const import ( ATTR_UNIT_IMPERIAL, ATTR_UNIT_METRIC, ATTRIBUTION, - COORDINATOR, DOMAIN, FORECAST_SENSOR_TYPES, MANUFACTURER, @@ -46,9 +45,7 @@ async def async_setup_entry( """Add AccuWeather entities from a config_entry.""" name: str = entry.data[CONF_NAME] - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - COORDINATOR - ] + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AccuWeatherSensor] = [] for sensor in SENSOR_TYPES: diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index 5feed5c1f34..df1e607d15d 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -8,7 +8,7 @@ from accuweather.const import ENDPOINT from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from .const import COORDINATOR, DOMAIN +from .const import DOMAIN @callback @@ -21,8 +21,8 @@ def async_register( async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" - remaining_requests = list(hass.data[DOMAIN].values())[0][ - COORDINATOR + remaining_requests = list(hass.data[DOMAIN].values())[ + 0 ].accuweather.requests_remaining return { diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index ee0ef69d666..9a2ba769a82 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -31,7 +31,6 @@ from .const import ( ATTR_FORECAST, ATTRIBUTION, CONDITION_CLASSES, - COORDINATOR, DOMAIN, MANUFACTURER, NAME, @@ -46,9 +45,7 @@ async def async_setup_entry( """Add a AccuWeather weather entity from a config_entry.""" name: str = entry.data[CONF_NAME] - coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - COORDINATOR - ] + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([AccuWeatherEntity(name, coordinator)]) diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index 749f516e44c..bbdee3cb6f9 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -4,7 +4,7 @@ from unittest.mock import Mock from aiohttp import ClientError -from homeassistant.components.accuweather.const import COORDINATOR, DOMAIN +from homeassistant.components.accuweather.const import DOMAIN from homeassistant.setup import async_setup_component from tests.common import get_system_health_info @@ -18,9 +18,7 @@ async def test_accuweather_system_health(hass, aioclient_mock): hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock( - accuweather=Mock(requests_remaining="42") - ) + hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="42")) info = await get_system_health_info(hass, DOMAIN) @@ -42,9 +40,7 @@ async def test_accuweather_system_health_fail(hass, aioclient_mock): hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = {} - hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock( - accuweather=Mock(requests_remaining="0") - ) + hass.data[DOMAIN]["0123xyz"] = Mock(accuweather=Mock(requests_remaining="0")) info = await get_system_health_info(hass, DOMAIN) From b46bcdeeb12416348a110cd9f257be3f470a0907 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 20 Jun 2021 23:23:22 -0700 Subject: [PATCH 449/750] Bump adb-shell to 0.3.4 (#52044) * Bump adb-shell to 0.3.4 * Bump adb-shell to 0.3.4 * Bump adb-shell to 0.3.4 --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index f338ac4683e..d1e379435a0 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.3.1", + "adb-shell[async]==0.3.4", "androidtv[async]==0.0.60", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/requirements_all.txt b/requirements_all.txt index bfd344407d1..1b783e3b785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -105,7 +105,7 @@ adafruit-circuitpython-dht==3.6.0 adafruit-circuitpython-mcp230xx==2.2.2 # homeassistant.components.androidtv -adb-shell[async]==0.3.1 +adb-shell[async]==0.3.4 # homeassistant.components.alarmdecoder adext==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c17635f8bb..c1817329bb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ abodepy==1.2.0 accuweather==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.3.1 +adb-shell[async]==0.3.4 # homeassistant.components.alarmdecoder adext==0.4.2 From 23719bbb5e345f28fdc4b0bfdd73acfc2145aab4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Jun 2021 09:44:29 +0200 Subject: [PATCH 450/750] Upgrade wled to 0.7.0 (#52017) --- homeassistant/components/wled/light.py | 9 ++++++--- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 46dba23b39e..533cb595638 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -5,6 +5,7 @@ from functools import partial from typing import Any, Tuple, cast import voluptuous as vol +from wled import Preset from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -222,9 +223,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if playlist == -1: playlist = None - preset: int | None = self.coordinator.data.state.preset - if preset == -1: - preset = None + preset: int | None = None + if isinstance(self.coordinator.data.state.preset, Preset): + preset = self.coordinator.data.state.preset.preset_id + elif self.coordinator.data.state.preset != -1: + preset = self.coordinator.data.state.preset segment = self.coordinator.data.state.segments[self._segment] return { diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 8a88cd1e843..237f6850b66 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.6.0"], + "requirements": ["wled==0.7.0"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 1b783e3b785..9c6c1e84967 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2365,7 +2365,7 @@ wirelesstagpy==0.4.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.6.0 +wled==0.7.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1817329bb6..c7d6c608255 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1283,7 +1283,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.6.0 +wled==0.7.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 From f29bcf7ff76fc164061b74957a3b76cfc5af0369 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Mon, 21 Jun 2021 03:09:41 -0500 Subject: [PATCH 451/750] Modern Forms light platform (#51857) * Add light platform to Modern Forms integration * cleanup setup * Code review cleanup --- .../components/modern_forms/__init__.py | 2 + .../components/modern_forms/const.py | 3 + .../components/modern_forms/light.py | 144 +++++++++++++++++ .../components/modern_forms/services.yaml | 25 +++ tests/components/modern_forms/test_light.py | 145 ++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 homeassistant/components/modern_forms/light.py create mode 100644 tests/components/modern_forms/test_light.py diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 4e80c85cd52..7eee8ea9ad8 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -12,6 +12,7 @@ from aiomodernforms import ( from aiomodernforms.models import Device as ModernFormsDeviceState from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST from homeassistant.core import HomeAssistant @@ -27,6 +28,7 @@ from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ + LIGHT_DOMAIN, FAN_DOMAIN, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modern_forms/const.py b/homeassistant/components/modern_forms/const.py index b151a637d75..9dbefcfc570 100644 --- a/homeassistant/components/modern_forms/const.py +++ b/homeassistant/components/modern_forms/const.py @@ -7,8 +7,11 @@ ATTR_MANUFACTURER = "manufacturer" OPT_ON = "on" OPT_SPEED = "speed" +OPT_BRIGHTNESS = "brightness" # Services +SERVICE_SET_LIGHT_SLEEP_TIMER = "set_light_sleep_timer" +SERVICE_CLEAR_LIGHT_SLEEP_TIMER = "clear_light_sleep_timer" SERVICE_SET_FAN_SLEEP_TIMER = "set_fan_sleep_timer" SERVICE_CLEAR_FAN_SLEEP_TIMER = "clear_fan_sleep_timer" diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py new file mode 100644 index 00000000000..4b3b675c9e0 --- /dev/null +++ b/homeassistant/components/modern_forms/light.py @@ -0,0 +1,144 @@ +"""Support for Modern Forms Fan lights.""" +from __future__ import annotations + +from typing import Any + +from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.entity_platform as entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from . import ( + ModernFormsDataUpdateCoordinator, + ModernFormsDeviceEntity, + modernforms_exception_handler, +) +from .const import ( + ATTR_SLEEP_TIME, + CLEAR_TIMER, + DOMAIN, + OPT_BRIGHTNESS, + OPT_ON, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + SERVICE_SET_LIGHT_SLEEP_TIMER, +) + +BRIGHTNESS_RANGE = (1, 255) + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Modern Forms platform from config entry.""" + + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + # if no light unit installed no light entity + if not coordinator.data.info.light_type: + return + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_LIGHT_SLEEP_TIMER, + { + vol.Required(ATTR_SLEEP_TIME): vol.All( + vol.Coerce(int), vol.Range(min=1, max=1440) + ), + }, + "async_set_light_sleep_timer", + ) + + platform.async_register_entity_service( + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + {}, + "async_clear_light_sleep_timer", + ) + + async_add_entities( + [ + ModernFormsLightEntity( + entry_id=config_entry.entry_id, coordinator=coordinator + ) + ] + ) + + +class ModernFormsLightEntity(LightEntity, ModernFormsDeviceEntity): + """Defines a Modern Forms light.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms light.""" + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=f"{coordinator.data.info.device_name} Light", + icon=None, + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + + @property + def brightness(self) -> int | None: + """Return the brightness of this light between 1..255.""" + return round( + percentage_to_ranged_value( + BRIGHTNESS_RANGE, self.coordinator.data.state.light_brightness + ) + ) + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self.coordinator.data.state.light_on) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {OPT_ON: LIGHT_POWER_ON} + + if ATTR_BRIGHTNESS in kwargs: + data[OPT_BRIGHTNESS] = ranged_value_to_percentage( + BRIGHTNESS_RANGE, kwargs[ATTR_BRIGHTNESS] + ) + + await self.coordinator.modern_forms.light(**data) + + @modernforms_exception_handler + async def async_set_light_sleep_timer( + self, + sleep_time: int, + ) -> None: + """Set a Modern Forms light sleep timer.""" + await self.coordinator.modern_forms.light(sleep=sleep_time * 60) + + @modernforms_exception_handler + async def async_clear_light_sleep_timer( + self, + ) -> None: + """Clear a Modern Forms light sleep timer.""" + await self.coordinator.modern_forms.light(sleep=CLEAR_TIMER) diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml index b90fec11bf1..d4f2f0e7997 100644 --- a/homeassistant/components/modern_forms/services.yaml +++ b/homeassistant/components/modern_forms/services.yaml @@ -1,3 +1,28 @@ +set_light_sleep_timer: + name: Set light sleep timer + description: Set a sleep timer on a Modern Forms light. + target: + entity: + integration: modern_forms + domain: light + fields: + sleep_time: + name: Sleep Time + description: Number of seconds to set the timer. + required: true + example: "900" + selector: + number: + min: 1 + max: 1440 + unit_of_measurement: minutes +clear_light_sleep_timer: + name: Clear light sleep timer + description: Clear the sleep timer on a Modern Forms light. + target: + entity: + integration: modern_forms + domain: light set_fan_sleep_timer: name: Set fan sleep timer description: Set a sleep timer on a Modern Forms fan. diff --git a/tests/components/modern_forms/test_light.py b/tests/components/modern_forms/test_light.py new file mode 100644 index 00000000000..29725ab4bcd --- /dev/null +++ b/tests/components/modern_forms/test_light.py @@ -0,0 +1,145 @@ +"""Tests for the Modern Forms light platform.""" +from unittest.mock import patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN +from homeassistant.components.modern_forms.const import ( + ATTR_SLEEP_TIME, + DOMAIN, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + SERVICE_SET_LIGHT_SLEEP_TIMER, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms lights.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("light.modernformsfan_light") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 128 + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan Light" + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.modernformsfan_light") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF" + + +async def test_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=False, + ) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(on=True, brightness=100) + + +async def test_sleep_timer_services( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the Modern Forms segments.""" + await init_integration(hass, aioclient_mock) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_LIGHT_SLEEP_TIMER, + {ATTR_ENTITY_ID: "light.modernformsfan_light", ATTR_SLEEP_TIME: 1}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(sleep=60) + + with patch("aiomodernforms.ModernFormsDevice.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_LIGHT_SLEEP_TIMER, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with(sleep=0) + + +async def test_light_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the Modern Forms lights.""" + + await init_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() + + aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.modernformsfan_light") + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + + +async def test_light_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the Moder Forms lights.""" + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( + "homeassistant.components.modern_forms.ModernFormsDevice.light", + side_effect=ModernFormsConnectionError, + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.modernformsfan_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.modernformsfan_light") + assert state.state == STATE_UNAVAILABLE From b916247e8e03db9eac2339dfd759514506ae543b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Jun 2021 14:49:51 +0200 Subject: [PATCH 452/750] Improve editing of device automation referring non added select entity (#52047) * Improve editing of device automation referring non added select entity * Update tests --- .../alarm_control_panel/device_action.py | 2 ++ .../components/select/device_action.py | 20 ++++++--------- .../components/select/device_condition.py | 14 +++++------ .../components/select/device_trigger.py | 14 ++++++----- homeassistant/helpers/entity.py | 2 +- tests/components/select/test_device_action.py | 15 +++++++++-- .../select/test_device_condition.py | 18 ++++++++++++- .../components/select/test_device_trigger.py | 25 ++++++++++++++++++- 8 files changed, 79 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index fc218a2c9c3..d92f9615c9a 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -112,6 +112,8 @@ async def async_get_action_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List action capabilities.""" + # We need to refer to the state directly because ATTR_CODE_ARM_REQUIRED is not a + # capability attribute state = hass.states.get(config[CONF_ENTITY_ID]) code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py index 656fca32970..ece3c981690 100644 --- a/homeassistant/components/select/device_action.py +++ b/homeassistant/components/select/device_action.py @@ -12,9 +12,10 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import Context, HomeAssistant +from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import get_capability from homeassistant.helpers.typing import ConfigType from .const import ATTR_OPTION, ATTR_OPTIONS, CONF_OPTION, DOMAIN, SERVICE_SELECT_OPTION @@ -65,16 +66,9 @@ async def async_get_action_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, Any]: """List action capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) - if state is None: - return {} + try: + options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or [] + except HomeAssistantError: + options = [] - return { - "extra_fields": vol.Schema( - { - vol.Required(CONF_OPTION): vol.In( - state.attributes.get(ATTR_OPTIONS, []) - ), - } - ) - } + return {"extra_fields": vol.Schema({vol.Required(CONF_OPTION): vol.In(options)})} diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index 444105075b1..ad82c432ce2 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -13,9 +13,10 @@ from homeassistant.const import ( CONF_FOR, CONF_TYPE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, HomeAssistantError, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.entity import get_capability from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .const import ATTR_OPTIONS, CONF_OPTION, DOMAIN @@ -72,16 +73,15 @@ async def async_get_condition_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, Any]: """List condition capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) - if state is None: - return {} + try: + options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or [] + except HomeAssistantError: + options = [] return { "extra_fields": vol.Schema( { - vol.Required(CONF_OPTION): vol.In( - state.attributes.get(ATTR_OPTIONS, []) - ), + vol.Required(CONF_OPTION): vol.In(options), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 164f420c122..84f61dfaec9 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -22,8 +22,9 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.entity import get_capability from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -88,15 +89,16 @@ async def async_get_trigger_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, Any]: """List trigger capabilities.""" - state = hass.states.get(config[CONF_ENTITY_ID]) - if state is None: - return {} + try: + options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or [] + except HomeAssistantError: + options = [] return { "extra_fields": vol.Schema( { - vol.Optional(CONF_FROM): vol.In(state.attributes.get(ATTR_OPTIONS, [])), - vol.Optional(CONF_TO): vol.In(state.attributes.get(ATTR_OPTIONS, [])), + vol.Optional(CONF_FROM): vol.In(options), + vol.Optional(CONF_TO): vol.In(options), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e9d1e1d2e07..187d53ea00b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -93,7 +93,7 @@ def async_generate_entity_id( return test_string -def get_capability(hass: HomeAssistant, entity_id: str, capability: str) -> str | None: +def get_capability(hass: HomeAssistant, entity_id: str, capability: str) -> Any | None: """Get a capability attribute of an entity. First try the statemachine, then entity registry. diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index 1cdffe9ae00..5c2486a4e26 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -94,7 +94,7 @@ async def test_action(hass: HomeAssistant) -> None: assert select_calls[0].data == {"entity_id": "select.entity", "option": "option1"} -async def test_get_trigger_capabilities(hass: HomeAssistant) -> None: +async def test_get_action_capabilities(hass: HomeAssistant) -> None: """Test we get the expected capabilities from a select action.""" config = { "platform": "device", @@ -106,7 +106,18 @@ async def test_get_trigger_capabilities(hass: HomeAssistant) -> None: # Test when entity doesn't exists capabilities = await async_get_action_capabilities(hass, config) - assert capabilities == {} + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "option", + "required": True, + "type": "select", + "options": [], + }, + ] # Mock an entity hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index fa2d2736e0f..d5ee88156cf 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -159,7 +159,23 @@ async def test_get_condition_capabilities(hass: HomeAssistant) -> None: # Test when entity doesn't exists capabilities = await async_get_condition_capabilities(hass, config) - assert capabilities == {} + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "option", + "required": True, + "type": "select", + "options": [], + }, + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + }, + ] # Mock an entity hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index df81a67b847..b0066e9ac22 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -184,7 +184,30 @@ async def test_get_trigger_capabilities(hass: HomeAssistant) -> None: # Test when entity doesn't exists capabilities = await async_get_trigger_capabilities(hass, config) - assert capabilities == {} + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "from", + "optional": True, + "type": "select", + "options": [], + }, + { + "name": "to", + "optional": True, + "type": "select", + "options": [], + }, + { + "name": "for", + "optional": True, + "type": "positive_time_period_dict", + "optional": True, + }, + ] # Mock an entity hass.states.async_set("select.test", "option1", {"options": ["option1", "option2"]}) From 077131df1a087c41e222680f543f963ab219780c Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Mon, 21 Jun 2021 21:36:00 +0200 Subject: [PATCH 453/750] Update climate.py (#52065) Adding tuya TRVs type Moes that need extra function in ZHA. https://github.com/home-assistant/core/issues/49378 adding _TYST11/_TZE200_cwnjrr72 that is missed then being added in zigpy. https://github.com/zigpy/zha-device-handlers/pull/931 is adding the _TZE200_b6wax7g0 and i shall adding the missed _TYST11_b6wax7g0 --- homeassistant/components/zha/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 475b0c5d0b8..3aa11d85516 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -614,8 +614,12 @@ class CentralitePearl(ZenWithinThermostat): manufacturers={ "_TZE200_ckud7u2l", "_TZE200_ywdxldoj", + "_TZE200_cwnjrr72", + "_TZE200_b6wax7g0", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", + "_TYST11_cwnjrr72", + "_TYST11_b6wax7g0", }, ) class MoesThermostat(Thermostat): From 8a9a141f3c6390776eff05addda69a3533c79a17 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 21 Jun 2021 16:45:47 -0400 Subject: [PATCH 454/750] Fix zwave_js migration logic (#52070) * Fix zwave_js migration logic * revert change to move tests to new module * Update tests/components/zwave_js/test_init.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/migrate.py | 6 ++- tests/components/zwave_js/test_init.py | 41 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index ea4b978cab5..397f7efba24 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -84,7 +84,11 @@ def async_migrate_old_entity( if entry.domain != platform or entry.unique_id in registered_unique_ids: continue - old_ent_value_id = ValueID.from_unique_id(entry.unique_id) + try: + old_ent_value_id = ValueID.from_unique_id(entry.unique_id) + # Skip non value ID based unique ID's (e.g. node status sensor) + except IndexError: + continue if value_id.is_same_value_different_endpoints(old_ent_value_id): existing_entity_entries.append(entry) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 840d1b15b4d..3d57c449549 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -369,6 +369,47 @@ async def test_old_entity_migration( assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None +async def test_different_endpoint_migration_status_sensor( + hass, hank_binary_switch_state, client, integration +): + """Test that the different endpoint migration logic skips over the status sensor.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_status_sensor" + entity_name = SENSOR_NAME.split(".")[1] + + # Create entity RegistryEntry using fake endpoint + old_unique_id = f"{client.driver.controller.home_id}.32.node_status" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == SENSOR_NAME + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for i in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that the RegistryEntry is using the same unique ID + entity_entry = ent_reg.async_get(SENSOR_NAME) + assert entity_entry.unique_id == old_unique_id + + async def test_skip_old_entity_migration_for_multiple( hass, hank_binary_switch_state, client, integration ): From d805e971b422a4908a319c6e224fe58be382fe06 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 22 Jun 2021 00:14:17 +0000 Subject: [PATCH 455/750] [ci skip] Translation update --- .../components/ambee/translations/hu.json | 26 +++++++++++++++++++ .../ambee/translations/sensor.hu.json | 9 +++++++ .../components/apple_tv/translations/hu.json | 2 +- .../components/arcam_fmj/translations/hu.json | 1 + .../azure_devops/translations/hu.json | 1 + .../components/blebox/translations/hu.json | 2 +- .../components/bond/translations/hu.json | 2 +- .../components/bosch_shc/translations/hu.json | 25 ++++++++++++++++++ .../components/bsblan/translations/hu.json | 2 +- .../components/canary/translations/hu.json | 2 +- .../components/cast/translations/hu.json | 4 +-- .../cloudflare/translations/hu.json | 2 +- .../components/denonavr/translations/hu.json | 1 + .../components/emonitor/translations/hu.json | 2 +- .../enphase_envoy/translations/hu.json | 2 +- .../forked_daapd/translations/hu.json | 1 + .../components/fritz/translations/hu.json | 18 +++++++++++++ .../components/fritzbox/translations/hu.json | 2 +- .../fritzbox_callmonitor/translations/hu.json | 1 + .../garages_amsterdam/translations/hu.json | 9 +++++++ .../components/goalzero/translations/hu.json | 4 ++- .../components/gogogate2/translations/hu.json | 1 + .../growatt_server/translations/hu.json | 11 ++++++++ .../huawei_lte/translations/hu.json | 2 +- .../translations/hu.json | 1 + .../components/isy994/translations/hu.json | 2 +- .../keenetic_ndms2/translations/hu.json | 1 + .../components/kodi/translations/hu.json | 2 +- .../components/kraken/translations/hu.json | 12 +++++++++ .../lutron_caseta/translations/hu.json | 1 + .../meteoclimatic/translations/hu.json | 11 ++++++++ .../modern_forms/translations/hu.json | 22 ++++++++++++++++ .../components/nzbget/translations/hu.json | 2 +- .../ovo_energy/translations/hu.json | 2 +- .../components/plugwise/translations/hu.json | 2 +- .../components/powerwall/translations/hu.json | 2 +- .../pvpc_hourly_pricing/translations/de.json | 6 ++--- .../rainmachine/translations/hu.json | 1 + .../components/roomba/translations/de.json | 4 +-- .../components/roomba/translations/hu.json | 2 +- .../components/samsungtv/translations/hu.json | 11 +++++--- .../screenlogic/translations/hu.json | 2 +- .../components/sia/translations/hu.json | 15 +++++++++++ .../components/smappee/translations/hu.json | 2 +- .../somfy_mylink/translations/hu.json | 2 +- .../components/sonarr/translations/hu.json | 2 +- .../components/songpal/translations/hu.json | 2 +- .../squeezebox/translations/hu.json | 2 +- .../components/syncthru/translations/hu.json | 1 + .../synology_dsm/translations/hu.json | 2 +- .../system_bridge/translations/hu.json | 5 ++++ .../components/unifi/translations/hu.json | 2 +- .../components/upnp/translations/hu.json | 2 +- .../components/wallbox/translations/hu.json | 20 ++++++++++++++ .../components/wilight/translations/hu.json | 1 + .../components/withings/translations/hu.json | 1 + .../xiaomi_aqara/translations/hu.json | 2 +- .../xiaomi_miio/translations/hu.json | 14 ++++++++-- .../yamaha_musiccast/translations/hu.json | 17 ++++++++++++ .../components/yeelight/translations/hu.json | 1 + .../components/zha/translations/hu.json | 2 +- 61 files changed, 272 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/ambee/translations/hu.json create mode 100644 homeassistant/components/ambee/translations/sensor.hu.json create mode 100644 homeassistant/components/bosch_shc/translations/hu.json create mode 100644 homeassistant/components/fritz/translations/hu.json create mode 100644 homeassistant/components/garages_amsterdam/translations/hu.json create mode 100644 homeassistant/components/growatt_server/translations/hu.json create mode 100644 homeassistant/components/kraken/translations/hu.json create mode 100644 homeassistant/components/meteoclimatic/translations/hu.json create mode 100644 homeassistant/components/modern_forms/translations/hu.json create mode 100644 homeassistant/components/sia/translations/hu.json create mode 100644 homeassistant/components/system_bridge/translations/hu.json create mode 100644 homeassistant/components/wallbox/translations/hu.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/hu.json diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json new file mode 100644 index 00000000000..556412764a2 --- /dev/null +++ b/homeassistant/components/ambee/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.hu.json b/homeassistant/components/ambee/translations/sensor.hu.json new file mode 100644 index 00000000000..aa86baf2722 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "high": "Magas", + "low": "Alacsony", + "very high": "Nagyon magas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 63bf29a73f1..72b334849a0 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -12,7 +12,7 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "title": "Apple TV sikeresen hozz\u00e1adva" diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 4af1181a265..e1784c4ad66 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -5,6 +5,7 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{host}", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 460b6132048..f85c6795fd5 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -8,6 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "{project_url}", "step": { "reauth": { "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 9649d70d976..97a6c1bdc18 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -8,7 +8,7 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd." }, - "flow_title": "BleBox eszk\u00f6z: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 868ef455f5d..535d3586b93 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -9,7 +9,7 @@ "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json new file mode 100644 index 00000000000..9cd2a0be6c1 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "host": "Hoszt" + } + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 50d250cc384..499a7d92331 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/canary/translations/hu.json b/homeassistant/components/canary/translations/hu.json index c2c70fdbf22..85dd503a175 100644 --- a/homeassistant/components/canary/translations/hu.json +++ b/homeassistant/components/canary/translations/hu.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 660e598a7cb..3b5840b1c14 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -9,10 +9,10 @@ "step": { "config": { "data": { - "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik." + "known_hosts": "Ismert hosztok" }, "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t.", - "title": "Google Cast" + "title": "Google Cast konfigur\u00e1ci\u00f3" }, "confirm": { "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json index fed6f22d536..a0f250376da 100644 --- a/homeassistant/components/cloudflare/translations/hu.json +++ b/homeassistant/components/cloudflare/translations/hu.json @@ -9,7 +9,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "records": { "data": { diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 41a1910bd56..2ae4bc69b55 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -8,6 +8,7 @@ "error": { "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json index 2d7d4218e7d..575e2a91d44 100644 --- a/homeassistant/components/emonitor/translations/hu.json +++ b/homeassistant/components/emonitor/translations/hu.json @@ -7,7 +7,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json index caef6a32c86..3449489bd87 100644 --- a/homeassistant/components/enphase_envoy/translations/hu.json +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -8,7 +8,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index ca90fad3048..3400984dcd6 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -7,6 +7,7 @@ "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json new file mode 100644 index 00000000000..eda37325071 --- /dev/null +++ b/homeassistant/components/fritz/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 44b68d5f540..630b15b990c 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -9,7 +9,7 @@ "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json index 8c2c34775e5..3255d205fa1 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/hu.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json @@ -7,6 +7,7 @@ "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/garages_amsterdam/translations/hu.json b/homeassistant/components/garages_amsterdam/translations/hu.json new file mode 100644 index 00000000000..c02cd4077ba --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index c876a55301f..ebc56c1bbe5 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json index cdc76a4145a..641046d7745 100644 --- a/homeassistant/components/gogogate2/translations/hu.json +++ b/homeassistant/components/gogogate2/translations/hu.json @@ -7,6 +7,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json new file mode 100644 index 00000000000..ff2c2fc87b5 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 815794133d2..5c045d0c6f3 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -13,7 +13,7 @@ "invalid_url": "\u00c9rv\u00e9nytelen URL", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index 063e0dad3c4..3de1b9d0117 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -7,6 +7,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 427a51157bf..ca8646ec584 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -8,7 +8,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json index b545f065ddc..c1d27e9ae07 100644 --- a/homeassistant/components/keenetic_ndms2/translations/hu.json +++ b/homeassistant/components/keenetic_ndms2/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index 64dbfac0c8b..48ea9d954bd 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -11,7 +11,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/kraken/translations/hu.json b/homeassistant/components/kraken/translations/hu.json new file mode 100644 index 00000000000..4901da74d90 --- /dev/null +++ b/homeassistant/components/kraken/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 921f5e83409..a8e62b37390 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/meteoclimatic/translations/hu.json b/homeassistant/components/meteoclimatic/translations/hu.json new file mode 100644 index 00000000000..893a6693c01 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json new file mode 100644 index 00000000000..b64bf60763a --- /dev/null +++ b/homeassistant/components/modern_forms/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json index 9eee6cc3be6..829fa03fe8e 100644 --- a/homeassistant/components/nzbget/translations/hu.json +++ b/homeassistant/components/nzbget/translations/hu.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 7bfd337cce5..14b3b23b2e7 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -5,7 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index d6d9012c21a..3d7de972fb0 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -8,7 +8,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index a2b8af13370..9f12342595a 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -9,7 +9,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 0af08c31961..04f5ddb8fba 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -9,10 +9,10 @@ "name": "Sensorname", "power": "Vertraglich vereinbarte Leistung (kW)", "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", - "tariff": "Vertragstarif (1, 2 oder 3 Perioden)" + "tariff": "Geltender Tarif nach geografischer Zone" }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. \nWeitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nW\u00e4hlen Sie den vertraglich vereinbarten Tarif basierend auf der Anzahl der Abrechnungsperioden pro Tag aus: \n - 1 Periode: Normal \n - 2 Perioden: Diskriminierung (Nachttarif) \n - 3 Perioden: Elektroauto (Nachttarif von 3 Perioden)", - "title": "Tarifauswahl" + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). ", + "title": "Sensoreinrichtung" } } }, diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index 48718980e2e..1ff7dc34b9c 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 053f3a31567..193469008e2 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -45,8 +45,8 @@ "host": "Host", "password": "Passwort" }, - "description": "Das Abrufen der BLID und des Kennworts erfolgt manuell. Befolgen Sie die in der Dokumentation beschriebenen Schritte unter: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" + "description": "W\u00e4hlen Sie einen Roomba oder Braava aus.", + "title": "Automatisch mit dem Ger\u00e4t verbinden" } } }, diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 931671f92d2..51957ba8847 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index 5c517c78d69..a720c5932ed 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -5,12 +5,17 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "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.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." + "not_supported": "Ez a Samsung k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Samsung TV: {model}", + "error": { + "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." + }, + "flow_title": "{device}", "step": { "confirm": { - "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.", + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {device} k\u00e9sz\u00fcl\u00e9ket? Ha kor\u00e1bban m\u00e9g csatlakoztattad a Home Assistantet, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json index 59e48fda273..f46ab499a29 100644 --- a/homeassistant/components/screenlogic/translations/hu.json +++ b/homeassistant/components/screenlogic/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json new file mode 100644 index 00000000000..f5538bfd6b5 --- /dev/null +++ b/homeassistant/components/sia/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "port": "Port", + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 15bfd4dc5d2..c2535713626 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -7,7 +7,7 @@ "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "local": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 08d0db14866..1cb4db9942a 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -8,7 +8,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index e3fa0b5ff21..160f9685308 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -9,7 +9,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json index aa55862c0aa..2bce32d0cb8 100644 --- a/homeassistant/components/songpal/translations/hu.json +++ b/homeassistant/components/songpal/translations/hu.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?" diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json index 216badd15c6..e9d7413ebfa 100644 --- a/homeassistant/components/squeezebox/translations/hu.json +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -10,7 +10,7 @@ "no_server_found": "Nem siker\u00fclt automatikusan felfedezni a szervert.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index 227b759ffaf..56e7c54203d 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -6,6 +6,7 @@ "error": { "invalid_url": "\u00c9rv\u00e9nytelen URL" }, + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index c26fd349f06..e5af260449a 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -8,7 +8,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 4602193850f..745b628b253 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -8,7 +8,7 @@ "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "service_unavailable": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 8b50de71f74..0bffeeaf154 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -8,7 +8,7 @@ "one": "hiba", "other": "" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wallbox/translations/hu.json b/homeassistant/components/wallbox/translations/hu.json new file mode 100644 index 00000000000..fd8db27da5e --- /dev/null +++ b/homeassistant/components/wallbox/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 26cdaf6a025..4ddb08bb975 100644 --- a/homeassistant/components/wilight/translations/hu.json +++ b/homeassistant/components/wilight/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, + "flow_title": "{name}", "step": { "confirm": { "title": "WiLight" diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index d39410b6d17..ec8c628a485 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -12,6 +12,7 @@ "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index 295fcef83fe..675ef24af3b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -12,7 +12,7 @@ "invalid_key": "\u00c9rv\u00e9nytelen kulcs", "invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm" }, - "flow_title": "Xiaomi Aqara K\u00f6zponti egys\u00e9g: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index e5cf4501608..2dfe53db303 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -2,13 +2,14 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { @@ -29,6 +30,15 @@ "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. K\u00e9rj\u00fck, vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", "title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" }, + "manual": { + "data": { + "host": "IP c\u00edm", + "token": "API Token" + } + }, + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json new file mode 100644 index 00000000000..0f973ce6dcc --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "confirm": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index ac463142359..cdb0839dd5a 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{model} {host}", "step": { "pick_device": { "data": { diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index aaa41429fde..896d4fbad30 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "ZHA: {n\u00e9v}", + "flow_title": "{name}", "step": { "port_config": { "data": { From 0eae0cca2bf841f2c2cb87fc602bc8afa3557174 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:42:03 -0400 Subject: [PATCH 456/750] Move zwave_js migration tests into new module (#52075) --- tests/components/zwave_js/test_init.py | 365 +-------------------- tests/components/zwave_js/test_migrate.py | 368 ++++++++++++++++++++++ 2 files changed, 369 insertions(+), 364 deletions(-) create mode 100644 tests/components/zwave_js/test_migrate.py diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3d57c449549..0b9009cd1d7 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -13,11 +13,7 @@ from homeassistant.config_entries import DISABLED_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import ( - AIR_TEMPERATURE_SENSOR, - EATON_RF9640_ENTITY, - NOTIFICATION_MOTION_BINARY_SENSOR, -) +from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY from tests.common import MockConfigEntry @@ -152,365 +148,6 @@ async def test_on_node_added_ready(hass, multisensor_6_state, client, integratio assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) -async def test_unique_id_migration_dupes( - hass, multisensor_6_state, client, integration -): - """Test we remove an entity when .""" - ent_reg = er.async_get(hass) - - entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id_1 = ( - f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" - ) - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id_1, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR - assert entity_entry.unique_id == old_unique_id_1 - - # Create entity RegistryEntry using b0 unique ID format - old_unique_id_2 = ( - f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00" - ) - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id_2, - suggested_object_id=f"{entity_name}_1", - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1" - assert entity_entry.unique_id == old_unique_id_2 - - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None - - -@pytest.mark.parametrize( - "id", - [ - ("52.52-49-00-Air temperature-00"), - ("52.52-49-0-Air temperature-00-00"), - ("52-49-0-Air temperature-00-00"), - ], -) -async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id): - """Test unique ID is migrated from old format to new.""" - ent_reg = er.async_get(hass) - - # Migrate version 1 - entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.{id}" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None - - -@pytest.mark.parametrize( - "id", - [ - ("32.32-50-00-value-W_Consumed"), - ("32.32-50-0-value-66049-W_Consumed"), - ("32-50-0-value-66049-W_Consumed"), - ], -) -async def test_unique_id_migration_property_key( - hass, hank_binary_switch_state, client, integration, id -): - """Test unique ID with property key is migrated from old format to new.""" - ent_reg = er.async_get(hass) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.{id}" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None - - -async def test_unique_id_migration_notification_binary_sensor( - hass, multisensor_6_state, client, integration -): - """Test unique ID is migrated from old format to new for a notification binary sensor.""" - ent_reg = er.async_get(hass) - - entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8" - entity_entry = ent_reg.async_get_or_create( - "binary_sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None - - -async def test_old_entity_migration( - hass, hank_binary_switch_state, client, integration -): - """Test old entity on a different endpoint is migrated to a new one.""" - node = Node(client, hank_binary_switch_state) - - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} - ) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] - - # Create entity RegistryEntry using fake endpoint - old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - device_id=device.id, - ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Do this twice to make sure re-interview doesn't do anything weird - for i in range(0, 2): - # Add a ready node, unique ID should be migrated - event = {"node": node} - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None - - -async def test_different_endpoint_migration_status_sensor( - hass, hank_binary_switch_state, client, integration -): - """Test that the different endpoint migration logic skips over the status sensor.""" - node = Node(client, hank_binary_switch_state) - - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} - ) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_status_sensor" - entity_name = SENSOR_NAME.split(".")[1] - - # Create entity RegistryEntry using fake endpoint - old_unique_id = f"{client.driver.controller.home_id}.32.node_status" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - device_id=device.id, - ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Do this twice to make sure re-interview doesn't do anything weird - for i in range(0, 2): - # Add a ready node, unique ID should be migrated - event = {"node": node} - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that the RegistryEntry is using the same unique ID - entity_entry = ent_reg.async_get(SENSOR_NAME) - assert entity_entry.unique_id == old_unique_id - - -async def test_skip_old_entity_migration_for_multiple( - hass, hank_binary_switch_state, client, integration -): - """Test that multiple entities of the same value but on a different endpoint get skipped.""" - node = Node(client, hank_binary_switch_state) - - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} - ) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] - - # Create two entity entrrys using different endpoints - old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id_1, - suggested_object_id=f"{entity_name}_1", - config_entry=integration, - original_name=f"{entity_name}_1", - device_id=device.id, - ) - assert entity_entry.entity_id == f"{SENSOR_NAME}_1" - assert entity_entry.unique_id == old_unique_id_1 - - # Create two entity entrrys using different endpoints - old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id_2, - suggested_object_id=f"{entity_name}_2", - config_entry=integration, - original_name=f"{entity_name}_2", - device_id=device.id, - ) - assert entity_entry.entity_id == f"{SENSOR_NAME}_2" - assert entity_entry.unique_id == old_unique_id_2 - # Add a ready node, unique ID should be migrated - event = {"node": node} - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is created using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - - # Check that the old entities stuck around because we skipped the migration step - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) - assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) - - -async def test_old_entity_migration_notification_binary_sensor( - hass, multisensor_6_state, client, integration -): - """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor.""" - node = Node(client, multisensor_6_state) - - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} - ) - - entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" - entity_entry = ent_reg.async_get_or_create( - "binary_sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - device_id=device.id, - ) - assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR - assert entity_entry.unique_id == old_unique_id - - # Do this twice to make sure re-interview doesn't do anything weird - for _ in range(0, 2): - # Add a ready node, unique ID should be migrated - event = {"node": node} - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" - assert entity_entry.unique_id == new_unique_id - assert ( - ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None - ) - - async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration): """Test we handle a non ready node added event.""" dev_reg = dr.async_get(hass) diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py new file mode 100644 index 00000000000..a1f60c31fce --- /dev/null +++ b/tests/components/zwave_js/test_migrate.py @@ -0,0 +1,368 @@ +"""Test the Z-Wave JS migration module.""" +import pytest +from zwave_js_server.model.node import Node + +from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR + + +async def test_unique_id_migration_dupes( + hass, multisensor_6_state, client, integration +): + """Test we remove an entity when .""" + ent_reg = er.async_get(hass) + + entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id_1 = ( + f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" + ) + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_1, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR + assert entity_entry.unique_id == old_unique_id_1 + + # Create entity RegistryEntry using b0 unique ID format + old_unique_id_2 = ( + f"{client.driver.controller.home_id}.52.52-49-0-Air temperature-00-00" + ) + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_2, + suggested_object_id=f"{entity_name}_1", + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == f"{AIR_TEMPERATURE_SENSOR}_1" + assert entity_entry.unique_id == old_unique_id_2 + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None + + +@pytest.mark.parametrize( + "id", + [ + ("52.52-49-00-Air temperature-00"), + ("52.52-49-0-Air temperature-00-00"), + ("52-49-0-Air temperature-00-00"), + ], +) +async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id): + """Test unique ID is migrated from old format to new.""" + ent_reg = er.async_get(hass) + + # Migrate version 1 + entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.{id}" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == AIR_TEMPERATURE_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None + + +@pytest.mark.parametrize( + "id", + [ + ("32.32-50-00-value-W_Consumed"), + ("32.32-50-0-value-66049-W_Consumed"), + ("32-50-0-value-66049-W_Consumed"), + ], +) +async def test_unique_id_migration_property_key( + hass, hank_binary_switch_state, client, integration, id +): + """Test unique ID with property key is migrated from old format to new.""" + ent_reg = er.async_get(hass) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.{id}" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == SENSOR_NAME + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, hank_binary_switch_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None + + +async def test_unique_id_migration_notification_binary_sensor( + hass, multisensor_6_state, client, integration +): + """Test unique ID is migrated from old format to new for a notification binary sensor.""" + ent_reg = er.async_get(hass) + + entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52.52-113-00-Home Security-Motion sensor status.8" + entity_entry = ent_reg.async_get_or_create( + "binary_sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + ) + assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Add a ready node, unique ID should be migrated + node = Node(client, multisensor_6_state) + event = {"node": node} + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None + + +async def test_old_entity_migration( + hass, hank_binary_switch_state, client, integration +): + """Test old entity on a different endpoint is migrated to a new one.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] + + # Create entity RegistryEntry using fake endpoint + old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == SENSOR_NAME + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for i in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None + + +async def test_different_endpoint_migration_status_sensor( + hass, hank_binary_switch_state, client, integration +): + """Test that the different endpoint migration logic skips over the status sensor.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_status_sensor" + entity_name = SENSOR_NAME.split(".")[1] + + # Create entity RegistryEntry using fake endpoint + old_unique_id = f"{client.driver.controller.home_id}.32.node_status" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == SENSOR_NAME + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for i in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that the RegistryEntry is using the same unique ID + entity_entry = ent_reg.async_get(SENSOR_NAME) + assert entity_entry.unique_id == old_unique_id + + +async def test_skip_old_entity_migration_for_multiple( + hass, hank_binary_switch_state, client, integration +): + """Test that multiple entities of the same value but on a different endpoint get skipped.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] + + # Create two entity entrrys using different endpoints + old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_1, + suggested_object_id=f"{entity_name}_1", + config_entry=integration, + original_name=f"{entity_name}_1", + device_id=device.id, + ) + assert entity_entry.entity_id == f"{SENSOR_NAME}_1" + assert entity_entry.unique_id == old_unique_id_1 + + # Create two entity entrrys using different endpoints + old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_2, + suggested_object_id=f"{entity_name}_2", + config_entry=integration, + original_name=f"{entity_name}_2", + device_id=device.id, + ) + assert entity_entry.entity_id == f"{SENSOR_NAME}_2" + assert entity_entry.unique_id == old_unique_id_2 + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is created using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + + # Check that the old entities stuck around because we skipped the migration step + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) + + +async def test_old_entity_migration_notification_binary_sensor( + hass, multisensor_6_state, client, integration +): + """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor.""" + node = Node(client, multisensor_6_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" + entity_entry = ent_reg.async_get_or_create( + "binary_sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for _ in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None + ) From 03ec7b3d0b4481912fb491c7c3c79080de6d0116 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 22 Jun 2021 06:22:38 +0200 Subject: [PATCH 457/750] ESPHome rework EsphomeEnumMapper for safe enum mappings (#51975) --- homeassistant/components/esphome/__init__.py | 30 ++++------ homeassistant/components/esphome/climate.py | 58 +++++++++---------- homeassistant/components/esphome/fan.py | 16 ++--- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 10 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 54 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 607af8cc47d..7783047a662 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -5,7 +5,7 @@ import asyncio import functools import logging import math -from typing import Callable +from typing import Generic, TypeVar from aioesphomeapi import ( APIClient, @@ -48,6 +48,7 @@ from .entry_data import RuntimeEntryData DOMAIN = "esphome" _LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T") STORAGE_VERSION = 1 @@ -721,30 +722,23 @@ def esphome_state_property(func): return _wrapper -class EsphomeEnumMapper: +class EsphomeEnumMapper(Generic[_T]): """Helper class to convert between hass and esphome enum values.""" - def __init__(self, func: Callable[[], dict[int, str]]) -> None: + def __init__(self, mapping: dict[_T, str]) -> None: """Construct a EsphomeEnumMapper.""" - self._func = func + # Add none mapping + mapping = {None: None, **mapping} + self._mapping = mapping + self._inverse: dict[str, _T] = {v: k for k, v in mapping.items()} - def from_esphome(self, value: int) -> str: + def from_esphome(self, value: _T | None) -> str | None: """Convert from an esphome int representation to a hass string.""" - return self._func()[value] + return self._mapping[value] - def from_hass(self, value: str) -> int: + def from_hass(self, value: str) -> _T: """Convert from a hass string to a esphome int representation.""" - inverse = {v: k for k, v in self._func().items()} - return inverse[value] - - -def esphome_map_enum(func: Callable[[], dict[int, str]]): - """Map esphome int enum values to hass string constants. - - This class has to be used as a decorator. This ensures the aioesphomeapi - import is only happening at runtime. - """ - return EsphomeEnumMapper(func) + return self._inverse[value] class EsphomeBaseEntity(Entity): diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 5d21d495ec2..15edcdd8150 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -58,7 +58,7 @@ from homeassistant.const import ( from . import ( EsphomeEntity, - esphome_map_enum, + EsphomeEnumMapper, esphome_state_property, platform_async_setup_entry, ) @@ -77,9 +77,8 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -@esphome_map_enum -def _climate_modes(): - return { +_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper( + { ClimateMode.OFF: HVAC_MODE_OFF, ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, @@ -87,11 +86,9 @@ def _climate_modes(): ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, ClimateMode.DRY: HVAC_MODE_DRY, } - - -@esphome_map_enum -def _climate_actions(): - return { +) +_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper( + { ClimateAction.OFF: CURRENT_HVAC_OFF, ClimateAction.COOLING: CURRENT_HVAC_COOL, ClimateAction.HEATING: CURRENT_HVAC_HEAT, @@ -99,11 +96,9 @@ def _climate_actions(): ClimateAction.DRYING: CURRENT_HVAC_DRY, ClimateAction.FAN: CURRENT_HVAC_FAN, } - - -@esphome_map_enum -def _fan_modes(): - return { +) +_FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper( + { ClimateFanMode.ON: FAN_ON, ClimateFanMode.OFF: FAN_OFF, ClimateFanMode.AUTO: FAN_AUTO, @@ -114,16 +109,15 @@ def _fan_modes(): ClimateFanMode.FOCUS: FAN_FOCUS, ClimateFanMode.DIFFUSE: FAN_DIFFUSE, } - - -@esphome_map_enum -def _swing_modes(): - return { +) +_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper( + { ClimateSwingMode.OFF: SWING_OFF, ClimateSwingMode.BOTH: SWING_BOTH, ClimateSwingMode.VERTICAL: SWING_VERTICAL, ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } +) class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @@ -156,7 +150,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): def hvac_modes(self) -> list[str]: """Return the list of available operation modes.""" return [ - _climate_modes.from_esphome(mode) + _CLIMATE_MODES.from_esphome(mode) for mode in self._static_info.supported_modes ] @@ -164,7 +158,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): def fan_modes(self): """Return the list of available fan modes.""" return [ - _fan_modes.from_esphome(mode) + _FAN_MODES.from_esphome(mode) for mode in self._static_info.supported_fan_modes ] @@ -177,7 +171,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): def swing_modes(self): """Return the list of available swing modes.""" return [ - _swing_modes.from_esphome(mode) + _SWING_MODES.from_esphome(mode) for mode in self._static_info.supported_swing_modes ] @@ -219,7 +213,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @esphome_state_property def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" - return _climate_modes.from_esphome(self._state.mode) + return _CLIMATE_MODES.from_esphome(self._state.mode) @esphome_state_property def hvac_action(self) -> str | None: @@ -227,12 +221,12 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): # HA has no support feature field for hvac_action if not self._static_info.supports_action: return None - return _climate_actions.from_esphome(self._state.action) + return _CLIMATE_ACTIONS.from_esphome(self._state.action) @esphome_state_property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return current fan setting.""" - return _fan_modes.from_esphome(self._state.fan_mode) + return _FAN_MODES.from_esphome(self._state.fan_mode) @esphome_state_property def preset_mode(self): @@ -240,9 +234,9 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): return PRESET_AWAY if self._state.away else PRESET_HOME @esphome_state_property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return current swing mode.""" - return _swing_modes.from_esphome(self._state.swing_mode) + return _SWING_MODES.from_esphome(self._state.swing_mode) @esphome_state_property def current_temperature(self) -> float | None: @@ -268,7 +262,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): """Set new target temperature (and operation mode if set).""" data = {"key": self._static_info.key} if ATTR_HVAC_MODE in kwargs: - data["mode"] = _climate_modes.from_hass(kwargs[ATTR_HVAC_MODE]) + data["mode"] = _CLIMATE_MODES.from_hass(kwargs[ATTR_HVAC_MODE]) if ATTR_TEMPERATURE in kwargs: data["target_temperature"] = kwargs[ATTR_TEMPERATURE] if ATTR_TARGET_TEMP_LOW in kwargs: @@ -280,7 +274,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" await self._client.climate_command( - key=self._static_info.key, mode=_climate_modes.from_hass(hvac_mode) + key=self._static_info.key, mode=_CLIMATE_MODES.from_hass(hvac_mode) ) async def async_set_preset_mode(self, preset_mode): @@ -291,11 +285,11 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" await self._client.climate_command( - key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode) + key=self._static_info.key, fan_mode=_FAN_MODES.from_hass(fan_mode) ) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" await self._client.climate_command( - key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode) + key=self._static_info.key, swing_mode=_SWING_MODES.from_hass(swing_mode) ) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 5272cdef5f1..b73df42c8dc 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -24,7 +24,7 @@ from homeassistant.util.percentage import ( from . import ( EsphomeEntity, - esphome_map_enum, + EsphomeEnumMapper, esphome_state_property, platform_async_setup_entry, ) @@ -47,12 +47,12 @@ async def async_setup_entry( ) -@esphome_map_enum -def _fan_directions(): - return { +_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper( + { FanDirection.FORWARD: DIRECTION_FORWARD, FanDirection.REVERSE: DIRECTION_REVERSE, } +) class EsphomeFan(EsphomeEntity, FanEntity): @@ -115,7 +115,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): async def async_set_direction(self, direction: str): """Set direction of the fan.""" await self._client.fan_command( - key=self._static_info.key, direction=_fan_directions.from_hass(direction) + key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction) ) # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property @@ -149,18 +149,18 @@ class EsphomeFan(EsphomeEntity, FanEntity): return self._static_info.supported_speed_levels @esphome_state_property - def oscillating(self) -> None: + def oscillating(self) -> bool | None: """Return the oscillation state.""" if not self._static_info.supports_oscillation: return None return self._state.oscillating @esphome_state_property - def current_direction(self) -> None: + def current_direction(self) -> str | None: """Return the current fan direction.""" if not self._static_info.supports_direction: return None - return _fan_directions.from_esphome(self._state.direction) + return _FAN_DIRECTIONS.from_esphome(self._state.direction) @property def supported_features(self) -> int: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 592ca616d04..b732713335e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.8.0"], + "requirements": ["aioesphomeapi==2.9.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 7b905aad148..d3dce2dea1b 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -25,7 +25,7 @@ from homeassistant.util import dt from . import ( EsphomeEntity, - esphome_map_enum, + EsphomeEnumMapper, esphome_state_property, platform_async_setup_entry, ) @@ -61,12 +61,12 @@ async def async_setup_entry( # pylint: disable=invalid-overridden-method -@esphome_map_enum -def _state_classes(): - return { +_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper( + { SensorStateClass.NONE: None, SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT, } +) class EsphomeSensor(EsphomeEntity, SensorEntity): @@ -122,7 +122,7 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): """Return the state class of this entity.""" if not self._static_info.state_class: return None - return _state_classes.from_esphome(self._static_info.state_class) + return _STATE_CLASSES.from_esphome(self._static_info.state_class) class EsphomeTextSensor(EsphomeEntity, SensorEntity): diff --git a/requirements_all.txt b/requirements_all.txt index 9c6c1e84967..5d19bd04e9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.8.0 +aioesphomeapi==2.9.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7d6c608255..b8ff4863197 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.8.0 +aioesphomeapi==2.9.0 # homeassistant.components.flo aioflo==0.4.1 From 9cd3ffbd47b647c856f25295b0a62ed8de086c71 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Tue, 22 Jun 2021 03:10:49 -0500 Subject: [PATCH 458/750] Modern Forms light platform code cleanup (#52058) --- homeassistant/components/modern_forms/light.py | 2 +- homeassistant/components/modern_forms/services.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index 4b3b675c9e0..2c8298f00da 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -80,7 +80,7 @@ async def async_setup_entry( ) -class ModernFormsLightEntity(LightEntity, ModernFormsDeviceEntity): +class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity): """Defines a Modern Forms light.""" def __init__( diff --git a/homeassistant/components/modern_forms/services.yaml b/homeassistant/components/modern_forms/services.yaml index d4f2f0e7997..ce3c29f39b5 100644 --- a/homeassistant/components/modern_forms/services.yaml +++ b/homeassistant/components/modern_forms/services.yaml @@ -8,7 +8,7 @@ set_light_sleep_timer: fields: sleep_time: name: Sleep Time - description: Number of seconds to set the timer. + description: Number of minutes to set the timer. required: true example: "900" selector: @@ -33,7 +33,7 @@ set_fan_sleep_timer: fields: sleep_time: name: Sleep Time - description: Number of seconds to set the timer. + description: Number of minutes to set the timer. required: true example: "900" selector: From 39bf304031fe3ab6d600cf54af3f9dbbc57c3191 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Tue, 22 Jun 2021 12:50:50 +0300 Subject: [PATCH 459/750] Static typing for PiHole (#51681) Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + homeassistant/components/pi_hole/__init__.py | 30 ++++++++++------ .../components/pi_hole/binary_sensor.py | 15 +++++--- .../components/pi_hole/config_flow.py | 30 +++++++++++----- homeassistant/components/pi_hole/sensor.py | 34 ++++++++++++++----- homeassistant/components/pi_hole/switch.py | 26 +++++++++----- mypy.ini | 11 ++++++ 7 files changed, 107 insertions(+), 40 deletions(-) diff --git a/.strict-typing b/.strict-typing index c87b0b270a8..1112dfba9ae 100644 --- a/.strict-typing +++ b/.strict-typing @@ -56,6 +56,7 @@ homeassistant.components.notify.* homeassistant.components.number.* homeassistant.components.onewire.* homeassistant.components.persistent_notification.* +homeassistant.components.pi_hole.* homeassistant.components.proximity.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 34fdc9978c1..ab9191b0f4a 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,11 +1,13 @@ """The pi_hole component.""" +from __future__ import annotations + import logging from hole import Hole from hole.exceptions import HoleError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -13,10 +15,12 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -60,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Pi-hole integration.""" hass.data[DOMAIN] = {} @@ -77,7 +81,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Pi-hole entry.""" name = entry.data[CONF_NAME] host = entry.data[CONF_HOST] @@ -109,7 +113,7 @@ async def async_setup_entry(hass, entry): _LOGGER.warning("Failed to connect: %s", ex) raise ConfigEntryNotReady from ex - async def async_update_data(): + async def async_update_data() -> None: """Fetch data from API endpoint.""" try: await api.get_data() @@ -133,7 +137,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Pi-hole entry.""" unload_ok = await hass.config_entries.async_unload_platforms( entry, _async_platforms(entry) @@ -144,7 +148,7 @@ async def async_unload_entry(hass, entry): @callback -def _async_platforms(entry): +def _async_platforms(entry: ConfigEntry) -> list[str]: """Return platforms to be loaded / unloaded.""" platforms = ["sensor"] if not entry.data[CONF_STATISTICS_ONLY]: @@ -157,7 +161,13 @@ def _async_platforms(entry): class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" - def __init__(self, api, coordinator, name, server_unique_id): + def __init__( + self, + api: Hole, + coordinator: DataUpdateCoordinator, + name: str, + server_unique_id: str, + ) -> None: """Initialize a Pi-hole entity.""" super().__init__(coordinator) self.api = api @@ -165,12 +175,12 @@ class PiHoleEntity(CoordinatorEntity): self._server_unique_id = server_unique_id @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return "mdi:pi-hole" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" return { "identifiers": {(DOMAIN, self._server_unique_id)}, diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 714a9f669c8..3c322d324d3 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -1,12 +1,17 @@ """Support for getting status from a Pi-hole system.""" from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PiHoleEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Pi-hole binary sensor.""" name = entry.data[CONF_NAME] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] @@ -25,16 +30,16 @@ class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity): """Representation of a Pi-hole binary sensor.""" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the sensor.""" return f"{self._server_unique_id}/Status" @property - def is_on(self): + def is_on(self) -> bool: """Return if the service is on.""" - return self.api.data.get("status") == "enabled" + return self.api.data.get("status") == "enabled" # type: ignore[no-any-return] diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 68f0ecbbb2c..cccd80472e3 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -1,5 +1,8 @@ """Config flow to configure the Pi-hole integration.""" +from __future__ import annotations + import logging +from typing import Any from hole import Hole from hole.exceptions import HoleError @@ -24,6 +27,7 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) @@ -34,19 +38,25 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" - self._config = None + self._config: dict = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" return await self.async_step_init(user_input) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by import.""" return await self.async_step_init(user_input, is_import=True) - async def async_step_init(self, user_input, is_import=False): + async def async_step_init( + self, user_input: dict[str, Any] | None, is_import: bool = False + ) -> FlowResult: """Handle init step of a flow.""" errors = {} @@ -131,7 +141,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_api_key(self, user_input=None): + async def async_step_api_key( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle step to setup API key.""" if user_input is not None: return self.async_create_entry( @@ -147,14 +159,16 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}), ) - async def _async_endpoint_existed(self, endpoint): + async def _async_endpoint_existed(self, endpoint: str) -> bool: existing_endpoints = [ f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" for entry in self._async_current_entries() ] return endpoint in existing_endpoints - async def _async_try_connect(self, host, location, tls, verify_tls): + async def _async_try_connect( + self, host: str, location: str, tls: bool, verify_tls: bool + ) -> None: session = async_get_clientsession(self.hass, verify_tls) pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls) await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 517e8cfcf17..95aee56f7cc 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,7 +1,16 @@ """Support for getting statistical data from a Pi-hole system.""" +from __future__ import annotations + +from typing import Any + +from hole import Hole from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity from .const import ( @@ -14,7 +23,9 @@ from .const import ( ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Pi-hole sensor.""" name = entry.data[CONF_NAME] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] @@ -34,7 +45,14 @@ async def async_setup_entry(hass, entry, async_add_entities): class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" - def __init__(self, api, coordinator, name, sensor_name, server_unique_id): + def __init__( + self, + api: Hole, + coordinator: DataUpdateCoordinator, + name: str, + sensor_name: str, + server_unique_id: str, + ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) @@ -46,27 +64,27 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): self._icon = variable_info[2] @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._name} {self._condition_name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the sensor.""" return f"{self._server_unique_id}/{self._condition_name}" @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return self._icon @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" return self._unit_of_measurement @property - def state(self): + def state(self) -> Any: """Return the state of the device.""" try: return round(self.api.data[self._condition], 2) @@ -74,6 +92,6 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): return self.api.data[self._condition] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the Pi-hole.""" return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index 955585243cf..b0c4b09c2e7 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -1,12 +1,18 @@ """Support for turning on and off Pi-hole system.""" +from __future__ import annotations + import logging +from typing import Any from hole.exceptions import HoleError import voluptuous as vol from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PiHoleEntity from .const import ( @@ -20,7 +26,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Pi-hole switch.""" name = entry.data[CONF_NAME] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] @@ -51,26 +59,26 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity): """Representation of a Pi-hole switch.""" @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the switch.""" return f"{self._server_unique_id}/Switch" @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return "mdi:pi-hole" @property - def is_on(self): + def is_on(self) -> bool: """Return if the service is on.""" - return self.api.data.get("status") == "enabled" + return self.api.data.get("status") == "enabled" # type: ignore[no-any-return] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the service.""" try: await self.api.enable() @@ -78,11 +86,11 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity): except HoleError as err: _LOGGER.error("Unable to enable Pi-hole: %s", err) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the service.""" await self.async_disable() - async def async_disable(self, duration=None): + async def async_disable(self, duration: Any = None) -> None: """Disable the service for a given duration.""" duration_seconds = True # Disable infinitely by default if duration is not None: diff --git a/mypy.ini b/mypy.ini index f80f2baa9e3..50257141179 100644 --- a/mypy.ini +++ b/mypy.ini @@ -627,6 +627,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.pi_hole.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.proximity.*] check_untyped_defs = true disallow_incomplete_defs = true From 52c142a82d56d26a8f49edcf640543fce7a96a05 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Jun 2021 11:59:20 +0200 Subject: [PATCH 460/750] Add support for color_mode white to MQTT light basic schema (#51484) * Add support for color_mode white to MQTT light basic schema * Add missing abbreviations --- .../components/mqtt/abbreviations.py | 2 + .../components/mqtt/light/schema_basic.py | 55 +++++-- tests/components/mqtt/test_light.py | 141 ++++++++++++++++++ 3 files changed, 186 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 6c572c093a3..1c94fae9d29 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -220,6 +220,8 @@ ABBREVIATIONS = { "uniq_id": "unique_id", "unit_of_meas": "unit_of_measurement", "val_tpl": "value_template", + "whit_cmd_t": "white_command_topic", + "whit_scl": "white_scale", "whit_val_cmd_t": "white_value_command_topic", "whit_val_scl": "white_value_scale", "whit_val_stat_t": "white_value_state_topic", diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 2030cfb8825..214da1dd7bf 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, + ATTR_WHITE, ATTR_WHITE_VALUE, ATTR_XY_COLOR, COLOR_MODE_BRIGHTNESS, @@ -22,6 +23,7 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_UNKNOWN, + COLOR_MODE_WHITE, COLOR_MODE_XY, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -29,6 +31,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_WHITE_VALUE, LightEntity, + valid_supported_color_modes, ) from homeassistant.const import ( CONF_NAME, @@ -86,6 +89,8 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" CONF_XY_VALUE_TEMPLATE = "xy_value_template" +CONF_WHITE_COMMAND_TOPIC = "white_command_topic" +CONF_WHITE_SCALE = "white_scale" CONF_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic" CONF_WHITE_VALUE_SCALE = "white_value_scale" CONF_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic" @@ -98,6 +103,7 @@ DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_WHITE_VALUE_SCALE = 255 +DEFAULT_WHITE_SCALE = 255 DEFAULT_ON_COMMAND_TYPE = "last" VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] @@ -168,6 +174,10 @@ PLATFORM_SCHEMA_BASIC = vol.All( vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -259,6 +269,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): CONF_RGBWW_COMMAND_TOPIC, CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, + CONF_WHITE_COMMAND_TOPIC, CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_WHITE_VALUE_STATE_TOPIC, CONF_XY_COMMAND_TOPIC, @@ -316,35 +327,40 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None ) self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None - self._supported_color_modes = set() + supported_color_modes = set() if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) self._color_mode = COLOR_MODE_COLOR_TEMP if topic[CONF_HS_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_HS) + supported_color_modes.add(COLOR_MODE_HS) self._color_mode = COLOR_MODE_HS if topic[CONF_RGB_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_RGB) + supported_color_modes.add(COLOR_MODE_RGB) self._color_mode = COLOR_MODE_RGB if topic[CONF_RGBW_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_RGBW) + supported_color_modes.add(COLOR_MODE_RGBW) self._color_mode = COLOR_MODE_RGBW if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_RGBWW) + supported_color_modes.add(COLOR_MODE_RGBWW) self._color_mode = COLOR_MODE_RGBWW + if topic[CONF_WHITE_COMMAND_TOPIC] is not None: + supported_color_modes.add(COLOR_MODE_WHITE) if topic[CONF_XY_COMMAND_TOPIC] is not None: - self._supported_color_modes.add(COLOR_MODE_XY) + supported_color_modes.add(COLOR_MODE_XY) self._color_mode = COLOR_MODE_XY - if len(self._supported_color_modes) > 1: + if len(supported_color_modes) > 1: self._color_mode = COLOR_MODE_UNKNOWN - if not self._supported_color_modes: + if not supported_color_modes: if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: self._color_mode = COLOR_MODE_BRIGHTNESS - self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + supported_color_modes.add(COLOR_MODE_BRIGHTNESS) else: self._color_mode = COLOR_MODE_ONOFF - self._supported_color_modes.add(COLOR_MODE_ONOFF) + supported_color_modes.add(COLOR_MODE_ONOFF) + + # Validate the color_modes configuration + self._supported_color_modes = valid_supported_color_modes(supported_color_modes) if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: self._legacy_mode = True @@ -817,7 +833,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # If brightness is being used instead of an on command, make sure # there is a brightness input. Either set the brightness to our # saved value or the maximum value if this is the first call - elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs: + elif ( + on_command_type == "brightness" + and ATTR_BRIGHTNESS not in kwargs + and ATTR_WHITE not in kwargs + ): kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 hs_color = kwargs.get(ATTR_HS_COLOR) @@ -971,6 +991,17 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): publish(CONF_EFFECT_COMMAND_TOPIC, effect) should_update |= set_optimistic(ATTR_EFFECT, effect) + if ATTR_WHITE in kwargs and self._topic[CONF_WHITE_COMMAND_TOPIC] is not None: + percent_white = float(kwargs[ATTR_WHITE]) / 255 + white_scale = self._config[CONF_WHITE_SCALE] + device_white_value = min(round(percent_white * white_scale), white_scale) + publish(CONF_WHITE_COMMAND_TOPIC, device_white_value) + should_update |= set_optimistic( + ATTR_BRIGHTNESS, + kwargs[ATTR_WHITE], + COLOR_MODE_WHITE, + ) + if ( ATTR_WHITE_VALUE in kwargs and self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 728656743e5..efd3b1b2424 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2268,6 +2268,83 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) +async def test_on_command_white(hass, mqtt_mock): + """Test sending commands for RGB + white light.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "tasmota_B94927/cmnd/POWER", + "value_template": "{{ value_json.POWER }}", + "payload_off": "OFF", + "payload_on": "ON", + "brightness_command_topic": "tasmota_B94927/cmnd/Dimmer", + "brightness_scale": 100, + "on_command_type": "brightness", + "brightness_value_template": "{{ value_json.Dimmer }}", + "rgb_command_topic": "tasmota_B94927/cmnd/Color2", + "rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}", + "white_command_topic": "tasmota_B94927/cmnd/White", + "white_scale": 100, + "color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}", + "qos": "0", + } + } + color_modes = ["rgb", "white"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_has_calls( + [ + call("tasmota_B94927/cmnd/Dimmer", "75", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white=255) + mqtt_mock.async_publish.assert_has_calls( + [ + call("tasmota_B94927/cmnd/White", "100", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white=64) + mqtt_mock.async_publish.assert_has_calls( + [ + call("tasmota_B94927/cmnd/White", "25", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_has_calls( + [ + call("tasmota_B94927/cmnd/Dimmer", "25", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_B94927/cmnd/POWER", "OFF", 0, False + ) + + async def test_explicit_color_mode(hass, mqtt_mock): """Test explicit color mode over mqtt.""" config = { @@ -2499,6 +2576,70 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes +async def test_white_state_update(hass, mqtt_mock): + """Test state updates for RGB + white light.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "tasmota_B94927/tele/STATE", + "command_topic": "tasmota_B94927/cmnd/POWER", + "value_template": "{{ value_json.POWER }}", + "payload_off": "OFF", + "payload_on": "ON", + "brightness_command_topic": "tasmota_B94927/cmnd/Dimmer", + "brightness_state_topic": "tasmota_B94927/tele/STATE", + "brightness_scale": 100, + "on_command_type": "brightness", + "brightness_value_template": "{{ value_json.Dimmer }}", + "rgb_command_topic": "tasmota_B94927/cmnd/Color2", + "rgb_state_topic": "tasmota_B94927/tele/STATE", + "rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}", + "white_command_topic": "tasmota_B94927/cmnd/White", + "white_scale": 100, + "color_mode_state_topic": "tasmota_B94927/tele/STATE", + "color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}", + "qos": "0", + } + } + color_modes = ["rgb", "white"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message( + hass, + "tasmota_B94927/tele/STATE", + '{"POWER":"ON","Dimmer":50,"Color":"0,0,0,128","White":50}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("rgb_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "white" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, + "tasmota_B94927/tele/STATE", + '{"POWER":"ON","Dimmer":50,"Color":"128,64,32,0","White":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + assert state.attributes.get("rgb_color") == (128, 64, 32) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_effect(hass, mqtt_mock): """Test effect.""" config = { From 456755c077fc0ee9b3a912054f7b2c81bbf773fa Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Tue, 22 Jun 2021 12:15:38 +0100 Subject: [PATCH 461/750] Adjust Growatt PV units from W to kW (#52021) --- homeassistant/components/growatt_server/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 881f0f46480..c8921d9e514 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -454,13 +454,13 @@ MIX_SENSOR_TYPES = { ), "mix_wattage_pv_1": ( "PV1 Wattage", - POWER_WATT, + POWER_KILO_WATT, "pPv1", {"device_class": DEVICE_CLASS_POWER}, ), "mix_wattage_pv_2": ( "PV2 Wattage", - POWER_WATT, + POWER_KILO_WATT, "pPv2", {"device_class": DEVICE_CLASS_POWER}, ), From d08129352f95ad87751ae5b0dff60a689290f0c4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 22 Jun 2021 15:44:53 +0200 Subject: [PATCH 462/750] Bump Nettigo Air Monitor library (#52085) --- homeassistant/components/nam/__init__.py | 6 +- homeassistant/components/nam/air_quality.py | 61 +++++++++++++-------- homeassistant/components/nam/config_flow.py | 2 +- homeassistant/components/nam/const.py | 27 ++++++--- homeassistant/components/nam/manifest.json | 2 +- homeassistant/components/nam/sensor.py | 48 +++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/test_air_quality.py | 29 +++++++++- tests/components/nam/test_sensor.py | 37 ++++++++++++- 10 files changed, 164 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 7dc6701217d..cf1df6b8ac8 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -9,8 +9,8 @@ from aiohttp.client_exceptions import ClientConnectorError import async_timeout from nettigo_air_monitor import ( ApiError, - DictToObj, InvalidSensorData, + NAMSensors, NettigoAirMonitor, ) @@ -75,7 +75,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL ) - async def _async_update_data(self) -> DictToObj: + async def _async_update_data(self) -> NAMSensors: """Update data via library.""" try: # Device firmware uses synchronous code and doesn't respond to http queries @@ -86,8 +86,6 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): except (ApiError, ClientConnectorError, InvalidSensorData) as error: raise UpdateFailed(error) from error - _LOGGER.debug(data) - return data @property diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py index 4f560740937..4c51003f3e6 100644 --- a/homeassistant/components/nam/air_quality.py +++ b/homeassistant/components/nam/air_quality.py @@ -1,17 +1,20 @@ """Support for the Nettigo Air Monitor air_quality service.""" from __future__ import annotations -from homeassistant.components.air_quality import AirQualityEntity +import logging +from typing import Union, cast + +from homeassistant.components.air_quality import DOMAIN as PLATFORM, AirQualityEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NAMDataUpdateCoordinator from .const import ( AIR_QUALITY_SENSORS, - ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_SDS011, DEFAULT_NAME, DOMAIN, SUFFIX_P1, @@ -20,6 +23,8 @@ from .const import ( PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -27,9 +32,23 @@ async def async_setup_entry( """Add a Nettigo Air Monitor entities from a config_entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + # Due to the change of the attribute name of one sensor, it is necessary to migrate + # the unique_id to the new name. + ent_reg = entity_registry.async_get(hass) + old_unique_id = f"{coordinator.unique_id}-sds" + new_unique_id = f"{coordinator.unique_id}-{ATTR_SDS011}" + if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id): + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + entities: list[NAMAirQuality] = [] for sensor in AIR_QUALITY_SENSORS: - if f"{sensor}{SUFFIX_P1}" in coordinator.data: + if getattr(coordinator.data, f"{sensor}{SUFFIX_P1}") is not None: entities.append(NAMAirQuality(coordinator, sensor)) async_add_entities(entities, False) @@ -49,25 +68,25 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): self.sensor_type = sensor_type @property - def particulate_matter_2_5(self) -> StateType: + def particulate_matter_2_5(self) -> int | None: """Return the particulate matter 2.5 level.""" - return round_state( - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") + return cast( + Union[int, None], + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}"), ) @property - def particulate_matter_10(self) -> StateType: + def particulate_matter_10(self) -> int | None: """Return the particulate matter 10 level.""" - return round_state( - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}") + return cast( + Union[int, None], + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}"), ) @property - def carbon_dioxide(self) -> StateType: + def carbon_dioxide(self) -> int | None: """Return the particulate matter 10 level.""" - return round_state( - getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None) - ) + return cast(Union[int, None], self.coordinator.data.mhz14a_carbon_dioxide) @property def available(self) -> bool: @@ -77,14 +96,8 @@ class NAMAirQuality(CoordinatorEntity, AirQualityEntity): # For a short time after booting, the device does not return values for all # sensors. For this reason, we mark entities for which data is missing as # unavailable. - return available and bool( - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}", None) + return ( + available + and getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") + is not None ) - - -def round_state(state: StateType) -> StateType: - """Round state.""" - if isinstance(state, float): - return round(state) - - return state diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index ccb5e6e6e84..a44f3f2ba6a 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -118,4 +118,4 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # when reading data from sensors. The nettigo-air-monitor library tries to get # the data 4 times, so we use a longer than usual timeout here. with async_timeout.timeout(30): - return cast(str, await nam.async_get_mac_address()) + return await nam.async_get_mac_address() diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 1c191019c04..318d1e15802 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -22,21 +22,27 @@ from homeassistant.const import ( from .model import SensorDescription +SUFFIX_P0: Final = "_p0" +SUFFIX_P1: Final = "_p1" +SUFFIX_P2: Final = "_p2" +SUFFIX_P4: Final = "_p4" + ATTR_BME280_HUMIDITY: Final = "bme280_humidity" ATTR_BME280_PRESSURE: Final = "bme280_pressure" ATTR_BME280_TEMPERATURE: Final = "bme280_temperature" ATTR_BMP280_PRESSURE: Final = "bmp280_pressure" ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature" -ATTR_DHT22_HUMIDITY: Final = "humidity" -ATTR_DHT22_TEMPERATURE: Final = "temperature" +ATTR_DHT22_HUMIDITY: Final = "dht22_humidity" +ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature" ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" -ATTR_MHZ14A_CARBON_DIOXIDE: Final = "conc_co2_ppm" +ATTR_SDS011: Final = "sds011" ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" ATTR_SIGNAL_STRENGTH: Final = "signal" -ATTR_SPS30_P0: Final = "sps30_p0" -ATTR_SPS30_P4: Final = "sps30_p4" +ATTR_SPS30: Final = "sps30" +ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}" +ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" ATTR_ENABLED: Final = "enabled" @@ -48,10 +54,15 @@ DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" -SUFFIX_P1: Final = "_p1" -SUFFIX_P2: Final = "_p2" +AIR_QUALITY_SENSORS: Final[dict[str, str]] = { + ATTR_SDS011: "SDS011", + ATTR_SPS30: "SPS30", +} -AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"} +MIGRATION_SENSORS: Final = [ + ("temperature", ATTR_DHT22_TEMPERATURE), + ("humidity", ATTR_DHT22_HUMIDITY), +] SENSORS: Final[dict[str, SensorDescription]] = { ATTR_BME280_HUMIDITY: { diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a003f3948e2..a1401c485de 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==0.2.6"], + "requirements": ["nettigo-air-monitor==1.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 6adb29d3efb..e088a8f00c1 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -2,21 +2,38 @@ from __future__ import annotations from datetime import timedelta -from typing import Any +import logging +from typing import cast -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as PLATFORM, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import ATTR_ENABLED, ATTR_LABEL, ATTR_UNIT, ATTR_UPTIME, DOMAIN, SENSORS +from .const import ( + ATTR_ENABLED, + ATTR_LABEL, + ATTR_UNIT, + ATTR_UPTIME, + DOMAIN, + MIGRATION_SENSORS, + SENSORS, +) PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -24,9 +41,24 @@ async def async_setup_entry( """Add a Nettigo Air Monitor entities from a config_entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + # Due to the change of the attribute name of two sensora, it is necessary to migrate + # the unique_ids to the new names. + ent_reg = entity_registry.async_get(hass) + for old_sensor, new_sensor in MIGRATION_SENSORS: + old_unique_id = f"{coordinator.unique_id}-{old_sensor}" + new_unique_id = f"{coordinator.unique_id}-{new_sensor}" + if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id): + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + sensors: list[NAMSensor | NAMSensorUptime] = [] for sensor in SENSORS: - if sensor in coordinator.data: + if getattr(coordinator.data, sensor) is not None: if sensor == ATTR_UPTIME: sensors.append(NAMSensorUptime(coordinator, sensor)) else: @@ -55,9 +87,9 @@ class NAMSensor(CoordinatorEntity, SensorEntity): self.sensor_type = sensor_type @property - def state(self) -> Any: + def state(self) -> StateType: """Return the state.""" - return getattr(self.coordinator.data, self.sensor_type) + return cast(StateType, getattr(self.coordinator.data, self.sensor_type)) @property def available(self) -> bool: @@ -67,8 +99,8 @@ class NAMSensor(CoordinatorEntity, SensorEntity): # For a short time after booting, the device does not return values for all # sensors. For this reason, we mark entities for which data is missing as # unavailable. - return available and bool( - getattr(self.coordinator.data, self.sensor_type, None) + return ( + available and getattr(self.coordinator.data, self.sensor_type) is not None ) diff --git a/requirements_all.txt b/requirements_all.txt index 5d19bd04e9d..70c8a2d886d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1008,7 +1008,7 @@ netdata==0.2.0 netdisco==2.8.3 # homeassistant.components.nam -nettigo-air-monitor==0.2.6 +nettigo-air-monitor==1.0.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8ff4863197..3dcdde675a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ nessclient==0.9.15 netdisco==2.8.3 # homeassistant.components.nam -nettigo-air-monitor==0.2.6 +nettigo-air-monitor==1.0.0 # homeassistant.components.nexia nexia==0.9.7 diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py index f9a213cec3e..5f687b8a29a 100644 --- a/tests/components/nam/test_air_quality.py +++ b/tests/components/nam/test_air_quality.py @@ -4,7 +4,13 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError -from homeassistant.components.air_quality import ATTR_CO2, ATTR_PM_2_5, ATTR_PM_10 +from homeassistant.components.air_quality import ( + ATTR_CO2, + ATTR_PM_2_5, + ATTR_PM_10, + DOMAIN as AIR_QUALITY_DOMAIN, +) +from homeassistant.components.nam.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -39,7 +45,7 @@ async def test_air_quality(hass): entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" state = hass.states.get("air_quality.nettigo_air_monitor_sps30") assert state @@ -146,3 +152,22 @@ async def test_manual_update_entity(hass): ) assert mock_get_data.call_count == 1 + + +async def test_unique_id_migration(hass): + """Test states of the unique_id migration.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + AIR_QUALITY_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-sds", + suggested_object_id="nettigo_air_monitor_sds011", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index b4c92c92e67..12fa0f01e44 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -143,7 +143,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity" state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") assert state @@ -154,7 +154,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature" state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") assert state @@ -302,3 +302,36 @@ async def test_manual_update_entity(hass): ) assert mock_get_data.call_count == 1 + + +async def test_unique_id_migration(hass): + """Test states of the unique_id migration.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-temperature", + suggested_object_id="nettigo_air_monitor_dht22_temperature", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-humidity", + suggested_object_id="nettigo_air_monitor_dht22_humidity", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_temperature" + + await init_integration(hass) + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-dht22_humidity" From 5795e76826c1a4f3f11fd96593a342b0b79068a6 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 22 Jun 2021 19:28:09 +0300 Subject: [PATCH 463/750] Migrate Switcher entity attributes to sensors (#51964) * Migrate attributes to sensors Migrate attributes to sensors * Fix pylint * Apply suggestions from code review Co-authored-by: Franck Nijhof * Add typing imports Co-authored-by: Franck Nijhof --- .coveragerc | 3 +- .../components/switcher_kis/__init__.py | 4 +- .../components/switcher_kis/const.py | 8 -- .../components/switcher_kis/sensor.py | 131 ++++++++++++++++++ .../components/switcher_kis/switch.py | 19 --- 5 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/switcher_kis/sensor.py diff --git a/.coveragerc b/.coveragerc index 0c2da893245..a58af4a7836 100644 --- a/.coveragerc +++ b/.coveragerc @@ -122,7 +122,7 @@ omit = homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py - homeassistant/components/braviatv/remote.py + homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py homeassistant/components/broadlink/const.py homeassistant/components/broadlink/remote.py @@ -992,6 +992,7 @@ omit = homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbot/switch.py + homeassistant/components/switcher_kis/sensor.py homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthing/__init__.py diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 6627e9d097c..ef196220656 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -8,7 +8,6 @@ import logging from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import CONF_DEVICE_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -65,7 +64,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return False hass.data[DOMAIN] = {DATA_DEVICE: device_data} - hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config)) + hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config)) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) @callback def device_updates(timestamp: datetime | None) -> None: diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py index da51fae4d1a..acd6c070337 100644 --- a/homeassistant/components/switcher_kis/const.py +++ b/homeassistant/components/switcher_kis/const.py @@ -1,5 +1,4 @@ """Constants for the Switcher integration.""" -from homeassistant.components.switch import ATTR_CURRENT_POWER_W DOMAIN = "switcher_kis" @@ -17,12 +16,5 @@ ATTR_REMAINING_TIME = "remaining_time" CONF_AUTO_OFF = "auto_off" CONF_TIMER_MINUTES = "timer_minutes" -DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { - "power_consumption": ATTR_CURRENT_POWER_W, - "electric_current": ATTR_ELECTRIC_CURRENT, - "remaining_time": ATTR_REMAINING_TIME, - "auto_off_set": ATTR_AUTO_OFF_SET, -} - SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py new file mode 100644 index 00000000000..037e7297cee --- /dev/null +++ b/homeassistant/components/switcher_kis/sensor.py @@ -0,0 +1,131 @@ +"""Switcher integration Sensor platform.""" +from __future__ import annotations + +from dataclasses import dataclass + +from aioswitcher.consts import WAITING_TEXT +from aioswitcher.devices import SwitcherV2Device + +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) +from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType, StateType + +from .const import DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE + + +@dataclass +class AttributeDescription: + """Class to describe a sensor.""" + + name: str + icon: str | None = None + unit: str | None = None + device_class: str | None = None + state_class: str | None = None + default_enabled: bool = True + default_value: float | int | str | None = None + + +POWER_SENSORS = { + "power_consumption": AttributeDescription( + name="Power Consumption", + unit=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + default_value=0, + ), + "electric_current": AttributeDescription( + name="Electric Current", + unit=ELECTRICAL_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + default_value=0.0, + ), +} + +TIME_SENSORS = { + "remaining_time": AttributeDescription( + name="Remaining Time", + icon="mdi:av-timer", + default_value="00:00:00", + ), + "auto_off_set": AttributeDescription( + name="Auto Shutdown", + icon="mdi:progress-clock", + default_enabled=False, + default_value="00:00:00", + ), +} + +SENSORS = {**POWER_SENSORS, **TIME_SENSORS} + + +async def async_setup_platform( + hass: HomeAssistant, + config: dict, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType, +) -> None: + """Set up Switcher sensor from config entry.""" + device_data = hass.data[DOMAIN][DATA_DEVICE] + + async_add_entities( + SwitcherSensorEntity(device_data, attribute, SENSORS[attribute]) + for attribute in SENSORS + ) + + +class SwitcherSensorEntity(SensorEntity): + """Representation of a Switcher sensor entity.""" + + def __init__( + self, + device_data: SwitcherV2Device, + attribute: str, + description: AttributeDescription, + ) -> None: + """Initialize the entity.""" + self._device_data = device_data + self.attribute = attribute + self.description = description + + # Entity class attributes + self._attr_name = f"{self._device_data.name} {self.description.name}" + self._attr_icon = self.description.icon + self._attr_unit_of_measurement = self.description.unit + self._attr_device_class = self.description.device_class + self._attr_entity_registry_enabled_default = self.description.default_enabled + self._attr_should_poll = False + + self._attr_unique_id = f"{self._device_data.device_id}-{self._device_data.mac_addr}-{self.attribute}" + + @property + def state(self) -> StateType: + """Return value of sensor.""" + value = getattr(self._device_data, self.attribute) + if value and value is not WAITING_TEXT: + return value + + return self.description.default_value + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data + ) + ) + + async def async_update_data(self, device_data: SwitcherV2Device) -> None: + """Update the entity data.""" + if device_data: + self._device_data = device_data + self.async_write_ha_state() diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 343d4dd8b13..27d9e16e1f8 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -8,7 +8,6 @@ from aioswitcher.consts import ( COMMAND_ON, STATE_OFF as SWITCHER_STATE_OFF, STATE_ON as SWITCHER_STATE_ON, - WAITING_TEXT, ) from aioswitcher.devices import SwitcherV2Device import voluptuous as vol @@ -23,7 +22,6 @@ from .const import ( CONF_AUTO_OFF, CONF_TIMER_MINUTES, DATA_DEVICE, - DEVICE_PROPERTIES_TO_HA_ATTRIBUTES, DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_TURN_ON_WITH_TIMER_NAME, @@ -124,23 +122,6 @@ class SwitcherControl(SwitchEntity): """Return True if entity is on.""" return self._state == SWITCHER_STATE_ON - @property - def current_power_w(self) -> int: - """Return the current power usage in W.""" - return self._device_data.power_consumption - - @property - def extra_state_attributes(self) -> dict: - """Return the optional state attributes.""" - attribs = {} - - for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items(): - value = getattr(self._device_data, prop) - if value and value is not WAITING_TEXT: - attribs[attr] = value - - return attribs - @property def available(self) -> bool: """Return True if entity is available.""" From 6814e9607adfbc67c3b73dd4d45b3c53c7ee4041 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Jun 2021 19:29:58 +0200 Subject: [PATCH 464/750] Improve deCONZ lights supported_color_modes and tests (#51933) * Improve deconz lights tests * Simplify attribute definition * Bump pydeconz to v80 --- homeassistant/components/deconz/light.py | 43 +- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_light.py | 1108 ++++++++++++----- 5 files changed, 827 insertions(+), 330 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index adcfb324ebe..4bfa1fa00b4 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -2,9 +2,6 @@ from __future__ import annotations -from pydeconz.group import DeconzGroup as Group -from pydeconz.light import Light - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -112,26 +109,21 @@ class DeconzBaseLight(DeconzDevice, LightEntity): super().__init__(device, gateway) self._attr_supported_color_modes = set() - self.update_features(self._device) - - def update_features(self, device: Light | Group) -> None: - """Calculate supported features of device.""" - supported_color_modes = self._attr_supported_color_modes if device.ct is not None: - supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) if device.hue is not None and device.sat is not None: - supported_color_modes.add(COLOR_MODE_HS) + self._attr_supported_color_modes.add(COLOR_MODE_HS) if device.xy is not None: - supported_color_modes.add(COLOR_MODE_XY) + self._attr_supported_color_modes.add(COLOR_MODE_XY) - if not supported_color_modes and device.brightness is not None: - supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + if not self._attr_supported_color_modes and device.brightness is not None: + self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) - if not supported_color_modes: - supported_color_modes.add(COLOR_MODE_ONOFF) + if not self._attr_supported_color_modes: + self._attr_supported_color_modes.add(COLOR_MODE_ONOFF) if device.brightness is not None: self._attr_supported_features |= SUPPORT_FLASH @@ -270,29 +262,8 @@ class DeconzGroup(DeconzBaseLight): def __init__(self, device, gateway): """Set up group and create an unique id.""" self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" - super().__init__(device, gateway) - for light_id in device.lights: - light = gateway.api.lights[light_id] - if light.ZHATYPE == Light.ZHATYPE: - self.update_features(light) - - for exclusive_color_mode in [COLOR_MODE_ONOFF, COLOR_MODE_BRIGHTNESS]: - if ( - exclusive_color_mode in self._attr_supported_color_modes - and len(self._attr_supported_color_modes) > 1 - ): - self._attr_supported_color_modes.remove(exclusive_color_mode) - - @property - def hs_color(self) -> tuple | None: - """Return the hs color value.""" - try: - return super().hs_color - except TypeError: - return None - @property def unique_id(self): """Return a unique identifier for this device.""" diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c4dfd0d4dfc..ad57b1bd903 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==79"], + "requirements": ["pydeconz==80"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 70c8a2d886d..ff5a7d69792 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1369,7 +1369,7 @@ pydaikin==2.4.3 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==79 +pydeconz==80 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dcdde675a4..b7ad93c1b37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -761,7 +761,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.4.3 # homeassistant.components.deconz -pydeconz==79 +pydeconz==80 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 28d7b1c59bd..f5adf6f74d6 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -4,20 +4,24 @@ from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS +from homeassistant.components.deconz.const import ATTR_ON, CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_EFFECT_LIST, ATTR_FLASH, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, COLOR_MODE_ONOFF, COLOR_MODE_XY, DOMAIN as LIGHT_DOMAIN, @@ -48,27 +52,657 @@ async def test_no_lights_or_groups(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): +@pytest.mark.parametrize( + "input,expected", + [ + ( # RGB light in color temp color mode + { + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": True, + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", + }, + { + "entity_id": "light.hue_go", + "state": STATE_ON, + "attributes": { + ATTR_BRIGHTNESS: 254, + ATTR_COLOR_TEMP: 375, + ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], + ATTR_SUPPORTED_COLOR_MODES: [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_XY, + ], + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + ATTR_MIN_MIREDS: 153, + ATTR_MAX_MIREDS: 500, + ATTR_SUPPORTED_FEATURES: 44, + "is_deconz_group": False, + }, + }, + ), + ( # RGB light in XY color mode + { + "colorcapabilities": 0, + "ctmax": 65535, + "ctmin": 0, + "etag": "74c91da78bbb5f4dc4d36edf4ad6857c", + "hascolor": True, + "lastannounced": "2021-01-27T18:05:38Z", + "lastseen": "2021-06-10T20:26Z", + "manufacturername": "Philips", + "modelid": "4090331P9_01", + "name": "Hue Ensis", + "state": { + "alert": "none", + "bri": 254, + "colormode": "xy", + "ct": 316, + "effect": "0", + "hue": 3096, + "on": True, + "reachable": True, + "sat": 48, + "xy": [0.427, 0.373], + }, + "swversion": "1.65.9_hB3217DF4", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-01", + }, + { + "entity_id": "light.hue_ensis", + "state": STATE_ON, + "attributes": { + ATTR_MIN_MIREDS: 140, + ATTR_MAX_MIREDS: 650, + ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], + ATTR_SUPPORTED_COLOR_MODES: [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_XY, + ], + ATTR_COLOR_MODE: COLOR_MODE_XY, + ATTR_BRIGHTNESS: 254, + ATTR_HS_COLOR: (29.691, 38.039), + ATTR_RGB_COLOR: (255, 206, 158), + ATTR_XY_COLOR: (0.427, 0.373), + "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: 44, + }, + }, + ), + ( # RGB light with only HS color mode + { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "LIDL xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + }, + { + "entity_id": "light.lidl_xmas_light", + "state": STATE_ON, + "attributes": { + ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], + ATTR_COLOR_MODE: COLOR_MODE_HS, + ATTR_BRIGHTNESS: 25, + ATTR_HS_COLOR: (294.938, 55.294), + ATTR_RGB_COLOR: (243, 113, 255), + ATTR_XY_COLOR: (0.357, 0.188), + "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: 44, + }, + }, + ), + ( # Tunable white light in CT color mode + { + "colorcapabilities": 16, + "ctmax": 454, + "ctmin": 153, + "etag": "576ffecbedb4abdc3d3f375fd8f17a9e", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LTW013", + "name": "Hue White Ambiance", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 396, + "on": True, + "reachable": True, + }, + "swversion": "1.46.13_r26312", + "type": "Color temperature light", + "uniqueid": "00:17:88:01:01:23:45:67-02", + }, + { + "entity_id": "light.hue_white_ambiance", + "state": STATE_ON, + "attributes": { + ATTR_MIN_MIREDS: 153, + ATTR_MAX_MIREDS: 454, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP], + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + ATTR_BRIGHTNESS: 254, + ATTR_COLOR_TEMP: 396, + "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: 40, + }, + }, + ), + ( # Dimmable light + { + "etag": "f88e87235e2abce62404edd99b1af323", + "hascolor": False, + "lastannounced": None, + "lastseen": "2021-06-10T20:26Z", + "manufacturername": "Philips", + "modelid": "LWO001", + "name": "Hue Filament", + "state": {"alert": "none", "bri": 254, "on": True, "reachable": True}, + "swversion": "1.55.8_r28815", + "type": "Dimmable light", + "uniqueid": "00:17:88:01:01:23:45:67-03", + }, + { + "entity_id": "light.hue_filament", + "state": STATE_ON, + "attributes": { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS], + ATTR_COLOR_MODE: COLOR_MODE_BRIGHTNESS, + ATTR_BRIGHTNESS: 254, + "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: 40, + }, + }, + ), + ( # On/Off light + { + "etag": "99c67fd8f0529c6c2aab94b45e4f6caa", + "hascolor": False, + "lastannounced": "2021-04-26T20:28:11Z", + "lastseen": "2021-06-10T21:15Z", + "manufacturername": "Unknown", + "modelid": "Unknown", + "name": "Simple Light", + "state": {"alert": "none", "on": True, "reachable": True}, + "swversion": "2.0", + "type": "Simple light", + "uniqueid": "00:15:8d:00:01:23:45:67-01", + }, + { + "entity_id": "light.simple_light", + "state": STATE_ON, + "attributes": { + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_ONOFF], + ATTR_COLOR_MODE: COLOR_MODE_ONOFF, + "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: 0, + }, + }, + ), + ], +) +async def test_lights(hass, aioclient_mock, input, expected): + """Test that different light entities are created with expected values.""" + data = {"lights": {"0": input}} + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + + light = hass.states.get(expected["entity_id"]) + assert light.state == expected["state"] + for attribute, expected_value in expected["attributes"].items(): + assert light.attributes[attribute] == expected_value + + await hass.config_entries.async_unload(config_entry.entry_id) + + states = hass.states.async_all() + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +async def test_light_state_change(hass, aioclient_mock, mock_deconz_websocket): + """Verify light can change state on websocket event.""" + data = { + "lights": { + "0": { + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": True, + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert hass.states.get("light.hue_go").state == STATE_ON + + event_changed_light = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "0", + "state": {"on": False}, + } + await mock_deconz_websocket(data=event_changed_light) + await hass.async_block_till_done() + + assert hass.states.get("light.hue_go").state == STATE_OFF + + +@pytest.mark.parametrize( + "input,expected", + [ + ( # Turn on light with short color loop + { + "light_on": False, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_BRIGHTNESS: 200, + ATTR_COLOR_TEMP: 200, + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + ATTR_EFFECT: EFFECT_COLORLOOP, + }, + }, + { + "bri": 200, + "ct": 200, + "transitiontime": 50, + "alert": "select", + "effect": "colorloop", + }, + ), + ( # Turn on light disabling color loop with long flashing + { + "light_on": False, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_XY_COLOR: (0.411, 0.351), + ATTR_FLASH: FLASH_LONG, + ATTR_EFFECT: "None", + }, + }, + { + "xy": (0.411, 0.351), + "alert": "lselect", + "effect": "none", + }, + ), + ( # Turn off light with short flashing + { + "light_on": True, + "service": SERVICE_TURN_OFF, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + }, + }, + { + "bri": 0, + "transitiontime": 50, + "alert": "select", + }, + ), + ( # Turn off light with long flashing + { + "light_on": True, + "service": SERVICE_TURN_OFF, + "call": {ATTR_ENTITY_ID: "light.hue_go", ATTR_FLASH: FLASH_LONG}, + }, + {"alert": "lselect"}, + ), + ( # Turn off light when light is already off is not supported + { + "light_on": False, + "service": SERVICE_TURN_OFF, + "call": { + ATTR_ENTITY_ID: "light.hue_go", + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + }, + }, + {}, + ), + ], +) +async def test_light_service_calls(hass, aioclient_mock, input, expected): + """Verify light can change state on websocket event.""" + data = { + "lights": { + "0": { + "colorcapabilities": 31, + "ctmax": 500, + "ctmin": 153, + "etag": "055485a82553e654f156d41c9301b7cf", + "hascolor": True, + "lastannounced": None, + "lastseen": "2021-06-10T20:25Z", + "manufacturername": "Philips", + "modelid": "LLC020", + "name": "Hue Go", + "state": { + "alert": "none", + "bri": 254, + "colormode": "ct", + "ct": 375, + "effect": "none", + "hue": 8348, + "on": input["light_on"], + "reachable": True, + "sat": 147, + "xy": [0.462, 0.4111], + }, + "swversion": "5.127.1.26420", + "type": "Extended color light", + "uniqueid": "00:17:88:01:01:23:45:67-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + + await hass.services.async_call( + LIGHT_DOMAIN, + input["service"], + input["call"], + blocking=True, + ) + if expected: + assert aioclient_mock.mock_calls[1][2] == expected + else: + assert len(aioclient_mock.mock_calls) == 1 # not called + + +async def test_ikea_default_transition_time(hass, aioclient_mock): + """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent.""" + data = { + "lights": { + "0": { + "colorcapabilities": 0, + "ctmax": 65535, + "ctmin": 0, + "etag": "9dd510cd474791481f189d2a68a3c7f1", + "hascolor": True, + "lastannounced": "2020-12-17T17:44:38Z", + "lastseen": "2021-01-11T18:36Z", + "manufacturername": "IKEA of Sweden", + "modelid": "TRADFRI bulb E27 WS opal 1000lm", + "name": "IKEA light", + "state": { + "alert": "none", + "bri": 156, + "colormode": "ct", + "ct": 250, + "on": True, + "reachable": True, + }, + "swversion": "2.0.022", + "type": "Color temperature light", + "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", + }, + }, + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.ikea_light", + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == { + "bri": 100, + "on": True, + "transitiontime": 0, + } + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.ikea_light", + ATTR_BRIGHTNESS: 100, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == { + "bri": 100, + "on": True, + "transitiontime": 50, + } + + +async def test_lidl_christmas_light(hass, aioclient_mock): """Test that lights or groups entities are created.""" + data = { + "lights": { + "0": { + "etag": "87a89542bf9b9d0aa8134919056844f8", + "hascolor": True, + "lastannounced": None, + "lastseen": "2020-12-05T22:57Z", + "manufacturername": "_TZE200_s8gkrkxk", + "modelid": "TS0601", + "name": "LIDL xmas light", + "state": { + "bri": 25, + "colormode": "hs", + "effect": "none", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + }, + "swversion": None, + "type": "Color dimmable light", + "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", + } + } + } + + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.lidl_xmas_light", + ATTR_HS_COLOR: (20, 30), + }, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76} + + assert hass.states.get("light.lidl_xmas_light") + + +async def test_configuration_tool(hass, aioclient_mock): + """Verify that configuration tool is not created.""" + data = { + "lights": { + "0": { + "etag": "26839cb118f5bf7ba1f2108256644010", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-11-22T11:27Z", + "manufacturername": "dresden elektronik", + "modelid": "ConBee II", + "name": "Configuration tool 1", + "state": {"reachable": True}, + "swversion": "0x264a0700", + "type": "Configuration tool", + "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + "input,expected", + [ + ( + { + "lights": ["1", "2", "3"], + }, + { + "entity_id": "light.group", + "state": ATTR_ON, + "attributes": { + ATTR_MIN_MIREDS: 153, + ATTR_MAX_MIREDS: 500, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + ATTR_BRIGHTNESS: 255, + ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], + "all_on": False, + "is_deconz_group": True, + ATTR_SUPPORTED_FEATURES: 44, + }, + }, + ), + ( + { + "lights": ["3", "1", "2"], + }, + { + "entity_id": "light.group", + "state": ATTR_ON, + "attributes": { + ATTR_MIN_MIREDS: 153, + ATTR_MAX_MIREDS: 500, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], + ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, + ATTR_BRIGHTNESS: 50, + ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], + "all_on": False, + "is_deconz_group": True, + ATTR_SUPPORTED_FEATURES: 44, + }, + }, + ), + ( + { + "lights": ["2", "3", "1"], + }, + { + "entity_id": "light.group", + "state": ATTR_ON, + "attributes": { + ATTR_MIN_MIREDS: 153, + ATTR_MAX_MIREDS: 500, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], + ATTR_COLOR_MODE: COLOR_MODE_XY, + ATTR_HS_COLOR: (52.0, 100.0), + ATTR_RGB_COLOR: (255, 221, 0), + ATTR_XY_COLOR: (0.5, 0.5), + "all_on": False, + "is_deconz_group": True, + ATTR_SUPPORTED_FEATURES: 44, + }, + }, + ), + ], +) +async def test_groups(hass, aioclient_mock, input, expected): + """Test that different group entities are created with expected values.""" data = { "groups": { - "1": { + "0": { "id": "Light group id", - "name": "Light group", + "name": "Group", "type": "LightGroup", "state": {"all_on": False, "any_on": True}, "action": {}, "scenes": [], - "lights": ["1", "2"], - }, - "2": { - "id": "Empty group id", - "name": "Empty group", - "type": "LightGroup", - "state": {}, - "action": {}, - "scenes": [], - "lights": [], + "lights": input["lights"], }, }, "lights": { @@ -76,7 +710,7 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): "name": "RGB light", "state": { "on": True, - "bri": 255, + "bri": 50, "colormode": "xy", "effect": "colorloop", "xy": (0.5, 0.5), @@ -89,190 +723,36 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): "ctmax": 454, "ctmin": 155, "name": "Tunable white light", - "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "state": { + "on": True, + "colormode": "ct", + "ct": 2500, + "reachable": True, + }, "type": "Tunable white light", "uniqueid": "00:00:00:00:00:00:00:01-00", }, "3": { - "name": "On off switch", - "type": "On/Off plug-in unit", - "state": {"reachable": True}, + "name": "Dimmable light", + "type": "Dimmable light", + "state": {"bri": 255, "on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, - "4": { - "name": "On off light", - "state": {"on": True, "reachable": True}, - "type": "On and Off light", - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, - "5": { - "ctmax": 1000, - "ctmin": 0, - "name": "Tunable white light with bad maxmin values", - "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, - "type": "Tunable white light", - "uniqueid": "00:00:00:00:00:00:00:04-00", - }, }, } with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 4 - rgb_light = hass.states.get("light.rgb_light") - assert rgb_light.state == STATE_ON - assert rgb_light.attributes[ATTR_BRIGHTNESS] == 255 - assert rgb_light.attributes[ATTR_XY_COLOR] == (0.5, 0.5) - assert rgb_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_XY] - assert rgb_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_XY - assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 44 - assert rgb_light.attributes["is_deconz_group"] is False - - tunable_white_light = hass.states.get("light.tunable_white_light") - assert tunable_white_light.state == STATE_ON - assert tunable_white_light.attributes[ATTR_COLOR_TEMP] == 2500 - assert tunable_white_light.attributes[ATTR_MAX_MIREDS] == 454 - assert tunable_white_light.attributes[ATTR_MIN_MIREDS] == 155 - assert tunable_white_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ - COLOR_MODE_COLOR_TEMP - ] - assert tunable_white_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP - assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 - - tunable_white_light_bad_maxmin = hass.states.get( - "light.tunable_white_light_with_bad_maxmin_values" - ) - assert tunable_white_light_bad_maxmin.state == STATE_ON - assert tunable_white_light_bad_maxmin.attributes[ATTR_COLOR_TEMP] == 2500 - assert tunable_white_light_bad_maxmin.attributes[ATTR_MAX_MIREDS] == 650 - assert tunable_white_light_bad_maxmin.attributes[ATTR_MIN_MIREDS] == 140 - assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 0 - - on_off_light = hass.states.get("light.on_off_light") - assert on_off_light.state == STATE_ON - assert on_off_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_ONOFF] - assert on_off_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_ONOFF - assert on_off_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 - - assert hass.states.get("light.light_group").state == STATE_ON - assert hass.states.get("light.light_group").attributes["all_on"] is False - - empty_group = hass.states.get("light.empty_group") - assert empty_group is None - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"on": False}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - assert hass.states.get("light.rgb_light").state == STATE_OFF - - # Verify service calls - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - # Service turn on light with short color loop - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_BRIGHTNESS: 200, - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - ATTR_EFFECT: EFFECT_COLORLOOP, - }, - blocking=True, - ) - assert aioclient_mock.mock_calls[1][2] == { - "bri": 200, - "transitiontime": 50, - "alert": "select", - "effect": "colorloop", - } - - # Service turn on light disabling color loop with long flashing - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_XY_COLOR: (0.411, 0.351), - ATTR_FLASH: FLASH_LONG, - ATTR_EFFECT: "None", - }, - blocking=True, - ) - assert aioclient_mock.mock_calls[2][2] == { - "xy": (0.411, 0.351), - "alert": "lselect", - "effect": "none", - } - - # Service turn on light with short flashing not supported - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - }, - blocking=True, - ) - assert len(aioclient_mock.mock_calls) == 3 # Not called - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "1", - "state": {"on": True}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - # Service turn off light with short flashing - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: "light.rgb_light", - ATTR_TRANSITION: 5, - ATTR_FLASH: FLASH_SHORT, - }, - blocking=True, - ) - assert aioclient_mock.mock_calls[3][2] == { - "bri": 0, - "transitiontime": 50, - "alert": "select", - } - - # Service turn off light with long flashing - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.rgb_light", ATTR_FLASH: FLASH_LONG}, - blocking=True, - ) - assert aioclient_mock.mock_calls[4][2] == {"alert": "lselect"} + group = hass.states.get(expected["entity_id"]) + assert group.state == expected["state"] + for attribute, expected_value in expected["attributes"].items(): + assert group.attributes[attribute] == expected_value await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 6 for state in states: assert state.state == STATE_UNAVAILABLE @@ -281,6 +761,155 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 +@pytest.mark.parametrize( + "input,expected", + [ + ( # Turn on group with short color loop + { + "lights": ["1", "2", "3"], + "group_on": False, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.group", + ATTR_BRIGHTNESS: 200, + ATTR_COLOR_TEMP: 200, + ATTR_TRANSITION: 5, + ATTR_FLASH: FLASH_SHORT, + ATTR_EFFECT: EFFECT_COLORLOOP, + }, + }, + { + "bri": 200, + "ct": 200, + "transitiontime": 50, + "alert": "select", + "effect": "colorloop", + }, + ), + ( # Turn on group with hs colors + { + "lights": ["1", "2", "3"], + "group_on": False, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.group", + ATTR_HS_COLOR: (250, 50), + }, + }, + { + "hue": 45510, + "on": True, + "sat": 127, + }, + ), + ( # Turn on group with short color loop + { + "lights": ["3", "2", "1"], + "group_on": False, + "service": SERVICE_TURN_ON, + "call": { + ATTR_ENTITY_ID: "light.group", + ATTR_HS_COLOR: (250, 50), + }, + }, + { + "hue": 45510, + "on": True, + "sat": 127, + }, + ), + ], +) +async def test_group_service_calls(hass, aioclient_mock, input, expected): + """Verify expected group web request from different service calls.""" + data = { + "groups": { + "0": { + "id": "Light group id", + "name": "Group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": input["group_on"]}, + "action": {}, + "scenes": [], + "lights": input["lights"], + }, + }, + "lights": { + "1": { + "name": "RGB light", + "state": { + "bri": 255, + "colormode": "xy", + "effect": "colorloop", + "hue": 53691, + "on": True, + "reachable": True, + "sat": 141, + "xy": (0.5, 0.5), + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + "2": { + "ctmax": 454, + "ctmin": 155, + "name": "Tunable white light", + "state": { + "on": True, + "colormode": "ct", + "ct": 2500, + "reachable": True, + }, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "name": "Dimmable light", + "type": "Dimmable light", + "state": {"bri": 254, "on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + }, + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/groups/0/action") + + await hass.services.async_call( + LIGHT_DOMAIN, + input["service"], + input["call"], + blocking=True, + ) + if expected: + assert aioclient_mock.mock_calls[1][2] == expected + else: + assert len(aioclient_mock.mock_calls) == 1 # not called + + +async def test_empty_group(hass, aioclient_mock): + """Verify that a group without a list of lights is not created.""" + data = { + "groups": { + "0": { + "id": "Empty group id", + "name": "Empty group", + "type": "LightGroup", + "state": {}, + "action": {}, + "scenes": [], + "lights": [], + }, + }, + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 0 + assert not hass.states.get("light.empty_group") + + async def test_disable_light_groups(hass, aioclient_mock): """Test disallowing light groups work.""" data = { @@ -344,109 +973,6 @@ async def test_disable_light_groups(hass, aioclient_mock): assert not hass.states.get("light.light_group") -async def test_configuration_tool(hass, aioclient_mock): - """Test that configuration tool is not created.""" - data = { - "lights": { - "0": { - "etag": "26839cb118f5bf7ba1f2108256644010", - "hascolor": False, - "lastannounced": None, - "lastseen": "2020-11-22T11:27Z", - "manufacturername": "dresden elektronik", - "modelid": "ConBee II", - "name": "Configuration tool 1", - "state": {"reachable": True}, - "swversion": "0x264a0700", - "type": "Configuration tool", - "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 0 - - -async def test_ikea_default_transition_time(hass, aioclient_mock): - """Verify that service calls to IKEA lights always extend with transition tinme 0 if absent.""" - data = { - "lights": { - "1": { - "manufacturername": "IKEA", - "name": "Dimmable light", - "state": {"on": True, "bri": 255, "reachable": True}, - "type": "Dimmable light", - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, - }, - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 100}, - blocking=True, - ) - assert aioclient_mock.mock_calls[1][2] == { - "bri": 100, - "on": True, - "transitiontime": 0, - } - - -async def test_lidl_christmas_light(hass, aioclient_mock): - """Test that lights or groups entities are created.""" - data = { - "lights": { - "0": { - "etag": "87a89542bf9b9d0aa8134919056844f8", - "hascolor": True, - "lastannounced": None, - "lastseen": "2020-12-05T22:57Z", - "manufacturername": "_TZE200_s8gkrkxk", - "modelid": "TS0601", - "name": "xmas light", - "state": { - "bri": 25, - "colormode": "hs", - "effect": "none", - "hue": 53691, - "on": True, - "reachable": True, - "sat": 141, - }, - "swversion": None, - "type": "Color dimmable light", - "uniqueid": "58:8e:81:ff:fe:db:7b:be-01", - } - } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/0/state") - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.xmas_light", - ATTR_HS_COLOR: (20, 30), - }, - blocking=True, - ) - assert aioclient_mock.mock_calls[1][2] == {"on": True, "hue": 3640, "sat": 76} - - assert hass.states.get("light.xmas_light") - - async def test_non_color_light_reports_color( hass, aioclient_mock, mock_deconz_websocket ): @@ -473,7 +999,7 @@ async def test_non_color_light_reports_color( "devicemembership": [], "etag": "81e42cf1b47affb72fa72bc2e25ba8bf", "lights": ["0", "1"], - "name": "All", + "name": "Group", "scenes": [], "state": {"all_on": False, "any_on": True}, "type": "LightGroup", @@ -535,7 +1061,7 @@ async def test_non_color_light_reports_color( await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 3 - assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] == 307 + assert hass.states.get("light.group").attributes[ATTR_COLOR_TEMP] == 250 # Updating a scene will return a faulty color value for a non-color light causing an exception in hs_color event_changed_light = { @@ -558,8 +1084,8 @@ async def test_non_color_light_reports_color( # Bug is fixed if we reach this point, but device won't have neither color temp nor color with pytest.raises(KeyError): - assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] - assert hass.states.get("light.all").attributes[ATTR_HS_COLOR] + assert hass.states.get("light.group").attributes[ATTR_COLOR_TEMP] + assert hass.states.get("light.group").attributes[ATTR_HS_COLOR] async def test_verify_group_supported_features(hass, aioclient_mock): @@ -568,7 +1094,7 @@ async def test_verify_group_supported_features(hass, aioclient_mock): "groups": { "1": { "id": "Group1", - "name": "group", + "name": "Group", "type": "LightGroup", "state": {"all_on": False, "any_on": True}, "action": {}, From e22893a206e6e51f1ea471a7ebda1c397b886ef6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 22 Jun 2021 20:34:25 +0200 Subject: [PATCH 465/750] Make attestation of supported features easier to read (deCONZ test) (#52096) Make is_deconz_group a constant --- homeassistant/components/deconz/light.py | 3 +- tests/components/deconz/test_light.py | 51 ++++++++++++++++-------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 4bfa1fa00b4..ac18d2de248 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -39,6 +39,7 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry CONTROLLER = ["Configuration tool"] +DECONZ_GROUP = "is_deconz_group" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -239,7 +240,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def extra_state_attributes(self): """Return the device state attributes.""" - return {"is_deconz_group": self._device.type == "LightGroup"} + return {DECONZ_GROUP: self._device.type == "LightGroup"} class DeconzLight(DeconzBaseLight): diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index f5adf6f74d6..2beb339dd4f 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.deconz.const import ATTR_ON, CONF_ALLOW_DECONZ_GROUPS +from homeassistant.components.deconz.light import DECONZ_GROUP from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -30,6 +31,9 @@ from homeassistant.components.light import ( FLASH_SHORT, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -98,8 +102,10 @@ async def test_no_lights_or_groups(hass, aioclient_mock): ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, ATTR_MIN_MIREDS: 153, ATTR_MAX_MIREDS: 500, - ATTR_SUPPORTED_FEATURES: 44, - "is_deconz_group": False, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION + | SUPPORT_FLASH + | SUPPORT_EFFECT, + DECONZ_GROUP: False, }, }, ), @@ -148,8 +154,10 @@ async def test_no_lights_or_groups(hass, aioclient_mock): ATTR_HS_COLOR: (29.691, 38.039), ATTR_RGB_COLOR: (255, 206, 158), ATTR_XY_COLOR: (0.427, 0.373), - "is_deconz_group": False, - ATTR_SUPPORTED_FEATURES: 44, + DECONZ_GROUP: False, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION + | SUPPORT_FLASH + | SUPPORT_EFFECT, }, }, ), @@ -186,8 +194,10 @@ async def test_no_lights_or_groups(hass, aioclient_mock): ATTR_HS_COLOR: (294.938, 55.294), ATTR_RGB_COLOR: (243, 113, 255), ATTR_XY_COLOR: (0.357, 0.188), - "is_deconz_group": False, - ATTR_SUPPORTED_FEATURES: 44, + DECONZ_GROUP: False, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION + | SUPPORT_FLASH + | SUPPORT_EFFECT, }, }, ), @@ -225,8 +235,8 @@ async def test_no_lights_or_groups(hass, aioclient_mock): ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, ATTR_BRIGHTNESS: 254, ATTR_COLOR_TEMP: 396, - "is_deconz_group": False, - ATTR_SUPPORTED_FEATURES: 40, + DECONZ_GROUP: False, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION | SUPPORT_FLASH, }, }, ), @@ -251,8 +261,8 @@ async def test_no_lights_or_groups(hass, aioclient_mock): ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_BRIGHTNESS], ATTR_COLOR_MODE: COLOR_MODE_BRIGHTNESS, ATTR_BRIGHTNESS: 254, - "is_deconz_group": False, - ATTR_SUPPORTED_FEATURES: 40, + DECONZ_GROUP: False, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION | SUPPORT_FLASH, }, }, ), @@ -276,7 +286,7 @@ async def test_no_lights_or_groups(hass, aioclient_mock): "attributes": { ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_ONOFF], ATTR_COLOR_MODE: COLOR_MODE_ONOFF, - "is_deconz_group": False, + DECONZ_GROUP: False, ATTR_SUPPORTED_FEATURES: 0, }, }, @@ -643,7 +653,7 @@ async def test_configuration_tool(hass, aioclient_mock): ATTR_BRIGHTNESS: 255, ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], "all_on": False, - "is_deconz_group": True, + DECONZ_GROUP: True, ATTR_SUPPORTED_FEATURES: 44, }, }, @@ -663,8 +673,10 @@ async def test_configuration_tool(hass, aioclient_mock): ATTR_BRIGHTNESS: 50, ATTR_EFFECT_LIST: [EFFECT_COLORLOOP], "all_on": False, - "is_deconz_group": True, - ATTR_SUPPORTED_FEATURES: 44, + DECONZ_GROUP: True, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION + | SUPPORT_FLASH + | SUPPORT_EFFECT, }, }, ), @@ -684,8 +696,10 @@ async def test_configuration_tool(hass, aioclient_mock): ATTR_RGB_COLOR: (255, 221, 0), ATTR_XY_COLOR: (0.5, 0.5), "all_on": False, - "is_deconz_group": True, - ATTR_SUPPORTED_FEATURES: 44, + DECONZ_GROUP: True, + ATTR_SUPPORTED_FEATURES: SUPPORT_TRANSITION + | SUPPORT_FLASH + | SUPPORT_EFFECT, }, }, ), @@ -1138,4 +1152,7 @@ async def test_verify_group_supported_features(hass, aioclient_mock): assert len(hass.states.async_all()) == 4 assert hass.states.get("light.group").state == STATE_ON - assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 44 + assert ( + hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT + ) From 04b425ed890182407d21b327a2d73c3aec8298a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Jun 2021 20:40:59 +0200 Subject: [PATCH 466/750] Use HS color instead of RGB color for Tasmota lights (#52052) --- homeassistant/components/tasmota/light.py | 49 ++--- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_light.py | 172 ++++-------------- 5 files changed, 56 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index edbb01eefe6..9af95049f79 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -12,13 +12,13 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_RGB_COLOR, + ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, COLOR_MODE_ONOFF, - COLOR_MODE_RGB, COLOR_MODE_WHITE, SUPPORT_EFFECT, SUPPORT_TRANSITION, @@ -90,7 +90,7 @@ class TasmotaLight( self._effect = None self._white_value = None self._flash_times = None - self._rgb = None + self._hs = None super().__init__( **kwds, @@ -111,10 +111,10 @@ class TasmotaLight( light_type = self._tasmota_entity.light_type if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: - # Mark RGB support for RGBW light because we don't have control over the + # Mark HS support for RGBW light because we don't have direct control over the # white channel, so the base component's RGB->RGBW translation does not work - self._supported_color_modes.add(COLOR_MODE_RGB) - self._color_mode = COLOR_MODE_RGB + self._supported_color_modes.add(COLOR_MODE_HS) + self._color_mode = COLOR_MODE_HS if light_type == LIGHT_TYPE_RGBW: self._supported_color_modes.add(COLOR_MODE_WHITE) @@ -149,8 +149,8 @@ class TasmotaLight( brightness = float(attributes["brightness"]) percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 - if "color" in attributes: - self._rgb = attributes["color"][0:3] + if "color_hs" in attributes: + self._hs = attributes["color_hs"] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -160,15 +160,15 @@ class TasmotaLight( percent_white = white_value / TASMOTA_BRIGHTNESS_MAX self._white_value = percent_white * 255 if self._tasmota_entity.light_type == LIGHT_TYPE_RGBW: - # Tasmota does not support RGBW mode, set mode to white or rgb + # Tasmota does not support RGBW mode, set mode to white or hs if self._white_value == 0: - self._color_mode = COLOR_MODE_RGB + self._color_mode = COLOR_MODE_HS else: self._color_mode = COLOR_MODE_WHITE elif self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: - # Tasmota does not support RGBWW mode, set mode to ct or rgb + # Tasmota does not support RGBWW mode, set mode to ct or hs if self._white_value == 0: - self._color_mode = COLOR_MODE_RGB + self._color_mode = COLOR_MODE_HS else: self._color_mode = COLOR_MODE_COLOR_TEMP @@ -210,21 +210,12 @@ class TasmotaLight( return self._tasmota_entity.effect_list @property - def rgb_color(self): - """Return the rgb color value.""" - if self._rgb is None: + def hs_color(self): + """Return the hs color value.""" + if self._hs is None: return None - rgb = self._rgb - # Tasmota's RGB color is adjusted for brightness, compensate - if self._brightness > 0: - red_compensated = clamp(round(rgb[0] / self._brightness * 255)) - green_compensated = clamp(round(rgb[1] / self._brightness * 255)) - blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - return [red_compensated, green_compensated, blue_compensated] + hs_color = self._hs + return [hs_color[0], hs_color[1]] @property def force_update(self): @@ -252,9 +243,9 @@ class TasmotaLight( attributes = {} - if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: - rgb = kwargs[ATTR_RGB_COLOR] - attributes["color"] = [rgb[0], rgb[1], rgb[2]] + if ATTR_HS_COLOR in kwargs and COLOR_MODE_HS in supported_color_modes: + hs_color = kwargs[ATTR_HS_COLOR] + attributes["color_hs"] = [hs_color[0], hs_color[1]] if ATTR_WHITE in kwargs and COLOR_MODE_WHITE in supported_color_modes: attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE]) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 13248cba47d..59b91b75903 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.16"], + "requirements": ["hatasmota==0.2.18"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index ff5a7d69792..1fe6fa8f2ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.16 +hatasmota==0.2.18 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7ad93c1b37..175c1fc2d77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ hangups==0.4.14 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.16 +hatasmota==0.2.18 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 0c0e0a4e566..e1ba2615742 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -189,8 +189,8 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["rgb"] - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("supported_color_modes") == ["hs"] + assert state.attributes.get("color_mode") == "hs" async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): @@ -223,8 +223,8 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["rgb", "white"] - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("supported_color_modes") == ["hs", "white"] + assert state.attributes.get("color_mode") == "hs" async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): @@ -257,7 +257,7 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("supported_color_modes") == ["color_temp", "hs"] assert state.attributes.get("color_mode") == "color_temp" @@ -292,7 +292,7 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("supported_color_modes") == ["color_temp", "hs"] assert state.attributes.get("color_mode") == "color_temp" @@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") @@ -447,7 +447,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}' @@ -460,13 +460,13 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Dimmer":50,"Color":"128,64,0","White":0}', + '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("hs_color") == (30, 100) + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' @@ -550,12 +550,12 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Color":"128,64,0","White":0}', + '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("hs_color") == (30, 100) + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' @@ -583,114 +583,8 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): # Setting white to 0 should clear the color_temp assert "white_value" not in state.attributes assert "color_temp" not in state.attributes - assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("color_mode") == "rgb" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("effect") == "Cycle down" - - async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - - state = hass.states.get("light.test") - assert state.state == STATE_ON - - async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - -async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota): - """Test state update via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config["rl"][0] = 2 - config["lt_st"] = 5 # 5 channel light (RGBCW) - config["so"]["17"] = 0 # Hex color in state updates - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - - state = hass.states.get("light.test") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert "color_mode" not in state.attributes - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - state = hass.states.get("light.test") - assert state.state == STATE_OFF - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert "color_mode" not in state.attributes - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("color_mode") == "color_temp" - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") - assert state.state == STATE_OFF - assert "color_mode" not in state.attributes - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("color_mode") == "color_temp" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"804000","White":0}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("color_mode") == "rgb" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"0080400000"}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (0, 255, 128) - assert state.attributes.get("color_mode") == "rgb" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert "white_value" not in state.attributes - # Setting white > 0 should clear the color - assert "rgb_color" not in state.attributes - assert state.attributes.get("color_mode") == "color_temp" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("color_temp") == 300 - assert state.attributes.get("color_mode") == "color_temp" - - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' - ) - state = hass.states.get("light.test") - assert state.state == STATE_ON - # Setting white to 0 should clear the white_value and color_temp - assert not state.attributes.get("white_value") - assert not state.attributes.get("color_temp") - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("hs_color") == (30, 100) + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -757,12 +651,12 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Color":"128,64,0","White":0}', + '{"POWER":"ON","HSBColor":"30,100,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("hs_color") == (30, 100) + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, @@ -771,8 +665,8 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (0, 0, 0) - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("hs_color") == (30, 100) + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' @@ -800,7 +694,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm # Setting white to 0 should clear the white_value and color_temp assert not state.attributes.get("white_value") assert not state.attributes.get("color_temp") - assert state.attributes.get("color_mode") == "rgb" + assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -955,10 +849,10 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + await common.async_turn_on(hass, "light.test", hs_color=[0, 100]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + "NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100", 0, False, ) @@ -1061,10 +955,10 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + await common.async_turn_on(hass, "light.test", hs_color=[180, 50]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + "NoDelay;Power1 ON;NoDelay;HsbColor1 180;NoDelay;HsbColor2 50", 0, False, ) @@ -1166,10 +1060,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + await common.async_turn_on(hass, "light.test", hs_color=[240, 75]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + "NoDelay;Power1 ON;NoDelay;HsbColor1 240;NoDelay;HsbColor2 75", 0, False, ) @@ -1356,7 +1250,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Dimmer":50, "Color":"0,255,0", "White":0}', + '{"POWER":"ON","Dimmer":50, "Color":"0,255,0","HSBColor":"120,100,50","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1367,7 +1261,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100", 0, False, ) @@ -1377,7 +1271,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Dimmer":100, "Color":"0,255,0"}', + '{"POWER":"ON","Dimmer":100, "Color":"0,255,0","HSBColor":"120,100,50"}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1388,7 +1282,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0", + "NoDelay;Fade2 1;NoDelay;Speed2 12;NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100", 0, False, ) @@ -1693,7 +1587,7 @@ async def test_discovery_update_reconfigure_light( state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["rgb"] + assert state.attributes.get("supported_color_modes") == ["hs"] async def test_availability_when_connection_lost( From ba3416b724699d00bd0824ed764f18a1aa3b3884 Mon Sep 17 00:00:00 2001 From: maurerle Date: Tue, 22 Jun 2021 20:54:27 +0200 Subject: [PATCH 467/750] Handle ConnectionError if proxmoxve host is not reachable (#51970) * handle ConnectionError if host is not reachable * import only needed exceptions fix pylint issue * don't overwrite built-in ConnectionError * fix typo --- homeassistant/components/proxmoxve/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 5777bb3054c..1b0d07c69a3 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -124,6 +124,9 @@ async def async_setup(hass: HomeAssistant, config: dict): except ConnectTimeout: _LOGGER.warning("Connection to host %s timed out during setup", host) continue + except requests.exceptions.ConnectionError: + _LOGGER.warning("Host %s is not reachable", host) + continue hass.data[PROXMOX_CLIENTS][host] = proxmox_client From b112b18848379cea80ae81bb60837384eb1a187c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 22 Jun 2021 21:16:50 +0200 Subject: [PATCH 468/750] Get running event loop in debugpy (#52091) --- homeassistant/components/debugpy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 72f2e8db067..613ecfd8ffa 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -1,7 +1,7 @@ """The Remote Python Debugger integration.""" from __future__ import annotations -from asyncio import Event, get_event_loop +from asyncio import Event, get_running_loop import logging from threading import Thread @@ -44,7 +44,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: call: ServiceCall | None = None, *, wait: bool = True ) -> None: """Enable asyncio debugging and start the debugger.""" - get_event_loop().set_debug(True) + get_running_loop().set_debug(True) debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) From de5431c03712d31be45c3a92959006126d514578 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 23 Jun 2021 00:09:30 +0000 Subject: [PATCH 469/750] [ci skip] Translation update --- .../demo/translations/select.no.json | 9 ++++++++ .../demo/translations/select.pl.json | 9 ++++++++ .../pvpc_hourly_pricing/translations/no.json | 21 ++++++++++++++++--- .../pvpc_hourly_pricing/translations/pl.json | 19 +++++++++++++++-- .../components/select/translations/nl.json | 14 +++++++++++++ .../components/select/translations/no.json | 14 +++++++++++++ .../components/select/translations/pl.json | 14 +++++++++++++ .../simplisafe/translations/no.json | 2 +- .../simplisafe/translations/pl.json | 2 +- .../components/wemo/translations/no.json | 5 +++++ 10 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.no.json create mode 100644 homeassistant/components/demo/translations/select.pl.json create mode 100644 homeassistant/components/select/translations/nl.json create mode 100644 homeassistant/components/select/translations/no.json create mode 100644 homeassistant/components/select/translations/pl.json diff --git a/homeassistant/components/demo/translations/select.no.json b/homeassistant/components/demo/translations/select.no.json new file mode 100644 index 00000000000..246195bfd26 --- /dev/null +++ b/homeassistant/components/demo/translations/select.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Lyshastighet", + "ludicrous_speed": "Ludicrous Speed", + "ridiculous_speed": "Latterlig hastighet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.pl.json b/homeassistant/components/demo/translations/select.pl.json new file mode 100644 index 00000000000..e90b2ccd0cb --- /dev/null +++ b/homeassistant/components/demo/translations/select.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Pr\u0119dko\u015b\u0107 \u015bwiat\u0142a", + "ludicrous_speed": "Absurdalna pr\u0119dko\u015b\u0107", + "ridiculous_speed": "Niewiarygodna pr\u0119dko\u015b\u0107" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json index 7eec443fcaa..7429626674e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Sensornavn", - "tariff": "Avtaletariff (1, 2 eller 3 perioder)" + "power": "Kontrahert effekt (kW)", + "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)", + "tariff": "Gjeldende tariff etter geografisk sone" }, - "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprising av elektrisitet (PVPC)](https://www.esios.ree.es/es/pvpc) i Spania.\nFor mer presis forklaring, bes\u00f8k [integrasjonsdokumenter](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nVelg den avtalte satsen basert p\u00e5 antall faktureringsperioder per dag:\n- 1 periode: normal\n- 2 perioder: diskriminering (nattlig rate)\n- 3 perioder: elbil (per natt rate p\u00e5 3 perioder)", - "title": "Tariffvalg" + "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Oppsett av sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Kontrahert effekt (kW)", + "power_p3": "Kontrahert kraft for dalperioden P3 (kW)", + "tariff": "Gjeldende tariff etter geografisk sone" + }, + "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Oppsett av sensor" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index f6606c570aa..d689fd9383a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Nazwa sensora", - "tariff": "Zakontraktowana taryfa (1, 2 lub 3 okresy)" + "power": "Moc zakontraktowana (kW)", + "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", + "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" }, "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", - "title": "Wyb\u00f3r taryfy" + "title": "Konfiguracja sensora" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Moc zakontraktowana (kW)", + "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", + "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" + }, + "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", + "title": "Konfiguracja sensora" } } } diff --git a/homeassistant/components/select/translations/nl.json b/homeassistant/components/select/translations/nl.json new file mode 100644 index 00000000000..76bf789bb2f --- /dev/null +++ b/homeassistant/components/select/translations/nl.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Verander {entity_name} optie" + }, + "condition_type": { + "selected_option": "Huidige {entity_name} geselecteerde optie" + }, + "trigger_type": { + "current_option_changed": "{entity_name} optie veranderd" + } + }, + "title": "Selecteer" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/no.json b/homeassistant/components/select/translations/no.json new file mode 100644 index 00000000000..1458b8aa5b4 --- /dev/null +++ b/homeassistant/components/select/translations/no.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Endre alternativet {entity_name}" + }, + "condition_type": { + "selected_option": "Gjeldende {entity_name} valgt alternativ" + }, + "trigger_type": { + "current_option_changed": "Alternativet {entity_name} er endret" + } + }, + "title": "Velg" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/pl.json b/homeassistant/components/select/translations/pl.json new file mode 100644 index 00000000000..102cfa68534 --- /dev/null +++ b/homeassistant/components/select/translations/pl.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Zmie\u0144 opcj\u0119 {entity_name}" + }, + "condition_type": { + "selected_option": "Aktualnie wybrana opcja dla {entity_name}" + }, + "trigger_type": { + "current_option_changed": "Zmieniono opcj\u0119 {entity_name}" + } + }, + "title": "Wybierz" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 32802248856..bc82715ad63 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -19,7 +19,7 @@ "data": { "password": "Passord" }, - "description": "Tilgangstokenet ditt har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble til kontoen din p\u00e5 nytt.", + "description": "Din tilgang har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 7793d51817a..3d99e1f5145 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -19,7 +19,7 @@ "data": { "password": "Has\u0142o" }, - "description": "Tw\u00f3j token dost\u0119pu wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.", + "description": "Tw\u00f3j dost\u0119pu wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { diff --git a/homeassistant/components/wemo/translations/no.json b/homeassistant/components/wemo/translations/no.json index 59958ac048d..c5ab2233b68 100644 --- a/homeassistant/components/wemo/translations/no.json +++ b/homeassistant/components/wemo/translations/no.json @@ -9,5 +9,10 @@ "description": "\u00d8nsker du \u00e5 sette opp Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo-knappen ble trykket i 2 sekunder" + } } } \ No newline at end of file From c31f267106e523af6f93f14d511e8e2fea474609 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Jun 2021 23:19:02 -0700 Subject: [PATCH 470/750] Add state class to powerwall (#52102) --- homeassistant/components/powerwall/sensor.py | 47 +++++--------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 982952a4830..d6c326593aa 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,7 +3,7 @@ import logging from tesla_powerwall import MeterType -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE from .const import ( @@ -64,20 +64,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class PowerWallChargeSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall charge sensor.""" - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE - - @property - def name(self): - """Device Name.""" - return "Powerwall Charge" - - @property - def device_class(self): - """Device Class.""" - return DEVICE_CLASS_BATTERY + _attr_name = "Powerwall Charge" + _attr_unit_of_measurement = PERCENTAGE + _attr_device_class = DEVICE_CLASS_BATTERY @property def unique_id(self): @@ -93,6 +82,10 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = ENERGY_KILO_WATT + _attr_device_class = DEVICE_CLASS_POWER + def __init__( self, meter: MeterType, @@ -107,26 +100,10 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): coordinator, site_info, status, device_type, powerwalls_serial_numbers ) self._meter = meter - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ENERGY_KILO_WATT - - @property - def name(self): - """Device Name.""" - return f"Powerwall {self._meter.value.title()} Now" - - @property - def device_class(self): - """Device Class.""" - return DEVICE_CLASS_POWER - - @property - def unique_id(self): - """Device Uniqueid.""" - return f"{self.base_unique_id}_{self._meter.value}_instant_power" + self._attr_name = f"Powerwall {self._meter.value.title()} Now" + self._attr_unique_id = ( + f"{self.base_unique_id}_{self._meter.value}_instant_power" + ) @property def state(self): From 6c4816567ca735c9e85f42760e98f7cef43393c9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Jun 2021 23:56:11 -0700 Subject: [PATCH 471/750] Add state class to Sense (#52104) --- homeassistant/components/sense/sensor.py | 227 +++++------------------ 1 file changed, 44 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 0af64f3f17d..d779870d37d 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,5 +1,5 @@ """Support for monitoring a Sense energy sensor.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_POWER, @@ -121,6 +121,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SenseActiveSensor(SensorEntity): """Implementation of a Sense energy sensor.""" + _attr_icon = ICON + _attr_unit_of_measurement = POWER_WATT + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_should_poll = False + _attr_available = False + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__( self, data, @@ -133,54 +140,12 @@ class SenseActiveSensor(SensorEntity): ): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME - self._name = f"{name} {name_type}" - self._unique_id = unique_id - self._available = False + self._attr_name = f"{name} {name_type}" + self._attr_unique_id = unique_id self._data = data self._sense_monitor_id = sense_monitor_id self._sensor_type = sensor_type self._is_production = is_production - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return the availability of the sensor.""" - return self._available - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return POWER_WATT - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """Return the device should not poll for updates.""" - return False async def async_added_to_hass(self): """Register callbacks.""" @@ -200,16 +165,22 @@ class SenseActiveSensor(SensorEntity): if self._is_production else self._data.active_power ) - if self._available and self._state == new_state: + if self._attr_available and self._attr_state == new_state: return - self._state = new_state - self._available = True + self._attr_state = new_state + self._attr_available = True self.async_write_ha_state() class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" + _attr_unit_of_measurement = VOLT + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_icon = ICON + _attr_should_poll = False + _attr_available = False + def __init__( self, data, @@ -218,53 +189,11 @@ class SenseVoltageSensor(SensorEntity): ): """Initialize the Sense sensor.""" line_num = index + 1 - self._name = f"L{line_num} Voltage" - self._unique_id = f"{sense_monitor_id}-L{line_num}" - self._available = False + self._attr_name = f"L{line_num} Voltage" + self._attr_unique_id = f"{sense_monitor_id}-L{line_num}" self._data = data self._sense_monitor_id = sense_monitor_id self._voltage_index = index - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return the availability of the sensor.""" - return self._available - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return VOLT - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """Return the device should not poll for updates.""" - return False async def async_added_to_hass(self): """Register callbacks.""" @@ -280,16 +209,21 @@ class SenseVoltageSensor(SensorEntity): def _async_update_from_data(self): """Update the sensor from the data. Must not do I/O.""" new_state = round(self._data.active_voltage[self._voltage_index], 1) - if self._available and self._state == new_state: + if self._attr_available and self._attr_state == new_state: return - self._available = True - self._state = new_state + self._attr_available = True + self._attr_state = new_state self.async_write_ha_state() class SenseTrendsSensor(SensorEntity): """Implementation of a Sense energy sensor.""" + _attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_icon = ICON + _attr_should_poll = False + def __init__( self, data, @@ -301,22 +235,14 @@ class SenseTrendsSensor(SensorEntity): ): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME - self._name = f"{name} {name_type}" - self._unique_id = unique_id - self._available = False + self._attr_name = f"{name} {name_type}" + self._attr_unique_id = unique_id self._data = data self._sensor_type = sensor_type self._coordinator = trends_coordinator self._is_production = is_production - self._state = None - self._unit_of_measurement = ENERGY_KILO_WATT_HOUR self._had_any_update = False - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def state(self): """Return the state of the sensor.""" @@ -327,31 +253,6 @@ class SenseTrendsSensor(SensorEntity): """Return if entity is available.""" return self._had_any_update and self._coordinator.last_update_success - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - @callback def _async_update(self): """Track if we had an update so we do not report zero data.""" @@ -373,61 +274,21 @@ class SenseTrendsSensor(SensorEntity): class SenseEnergyDevice(SensorEntity): """Implementation of a Sense energy device.""" + _attr_available = False + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = POWER_WATT + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_device_class = DEVICE_CLASS_POWER + _attr_should_poll = False + def __init__(self, sense_devices_data, device, sense_monitor_id): """Initialize the Sense binary sensor.""" - self._name = f"{device['name']} {CONSUMPTION_NAME}" + self._attr_name = f"{device['name']} {CONSUMPTION_NAME}" self._id = device["id"] - self._available = False self._sense_monitor_id = sense_monitor_id - self._unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}" - self._icon = sense_to_mdi(device["icon"]) + self._attr_unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}" + self._attr_icon = sense_to_mdi(device["icon"]) self._sense_devices_data = sense_devices_data - self._state = None - - @property - def state(self): - """Return the wattage of the sensor.""" - return self._state - - @property - def available(self): - """Return the availability of the sensor.""" - return self._available - - @property - def name(self): - """Return the name of the power sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of the power sensor.""" - return self._unique_id - - @property - def icon(self): - """Return the icon of the power sensor.""" - return self._icon - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return POWER_WATT - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def device_class(self): - """Return the device class of the power sensor.""" - return DEVICE_CLASS_POWER - - @property - def should_poll(self): - """Return the device should not poll for updates.""" - return False async def async_added_to_hass(self): """Register callbacks.""" @@ -447,8 +308,8 @@ class SenseEnergyDevice(SensorEntity): new_state = 0 else: new_state = int(device_data["w"]) - if self._available and self._state == new_state: + if self._attr_available and self._attr_state == new_state: return - self._state = new_state - self._available = True + self._attr_state = new_state + self._attr_available = True self.async_write_ha_state() From 29bfb4b0463ded35131411a8e40406fa2e00c94c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 23 Jun 2021 11:30:42 +0200 Subject: [PATCH 472/750] Xiaomi_miio fan percentage based speeds and preset_modes (#51791) --- homeassistant/components/xiaomi_miio/fan.py | 467 +++++++++++++++++++- 1 file changed, 447 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index a485654e638..a5a5122ea07 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -3,6 +3,7 @@ import asyncio from enum import Enum from functools import partial import logging +import math from miio import ( AirFresh, @@ -40,6 +41,7 @@ from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, ) @@ -53,6 +55,10 @@ from homeassistant.const import ( CONF_TOKEN, ) import homeassistant.helpers.config_validation as cv +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from .const import ( CONF_DEVICE, @@ -157,7 +163,6 @@ ATTR_DRY = "dry" # Air Humidifier CA4 ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" ATTR_FAHRENHEIT = "fahrenheit" -ATTR_FAULT = "fault" # Air Fresh ATTR_CO2 = "co2" @@ -332,10 +337,15 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = { } OPERATION_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] +PRESET_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] OPERATION_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] +PRESET_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO +PRESET_MODES_AIRPURIFIER_PRO_V7 = PRESET_MODES_AIRPURIFIER_PRO OPERATION_MODES_AIRPURIFIER_2S = ["Auto", "Silent", "Favorite"] +PRESET_MODES_AIRPURIFIER_2S = ["Auto", "Silent", "Favorite"] OPERATION_MODES_AIRPURIFIER_3 = ["Auto", "Silent", "Favorite", "Fan"] +PRESET_MODES_AIRPURIFIER_3 = ["Auto", "Silent", "Favorite", "Fan"] OPERATION_MODES_AIRPURIFIER_V3 = [ "Auto", "Silent", @@ -345,7 +355,19 @@ OPERATION_MODES_AIRPURIFIER_V3 = [ "High", "Strong", ] +PRESET_MODES_AIRPURIFIER_V3 = [ + "Auto", + "Silent", + "Favorite", + "Idle", + "Medium", + "High", + "Strong", +] OPERATION_MODES_AIRFRESH = ["Auto", "Silent", "Interval", "Low", "Middle", "Strong"] +PRESET_MODES_AIRFRESH = ["Auto", "Interval"] +PRESET_MODES_AIRHUMIDIFIER = ["Auto"] +PRESET_MODES_AIRHUMIDIFIER_CA4 = ["Auto"] SUCCESS = ["ok"] @@ -635,11 +657,42 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): self._state_attrs = {ATTR_MODEL: self._model} self._device_features = FEATURE_SET_CHILD_LOCK self._skip_update = False + self._supported_features = 0 + self._speed_count = 100 + self._preset_modes = [] + # the speed_list attribute is deprecated, support will end with release 2021.7 + self._speed_list = [] @property def supported_features(self): """Flag supported features.""" - return SUPPORT_SET_SPEED + return self._supported_features + + # the speed_list attribute is deprecated, support will end with release 2021.7 + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def speed_count(self): + """Return the number of speeds of the fan supported.""" + return self._speed_count + + @property + def preset_modes(self) -> list: + """Get the list of available preset modes.""" + return self._preset_modes + + @property + def percentage(self): + """Return the percentage based speed of the fan.""" + return None + + @property + def preset_mode(self): + """Return the percentage based speed of the fan.""" + return None @property def should_poll(self): @@ -701,9 +754,14 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): **kwargs, ) -> None: """Turn the device on.""" + # Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented if speed: - # If operation mode was set the device must not be turned on. - result = await self.async_set_speed(speed) + await self.async_set_speed(speed) + # If operation mode was set the device must not be turned on. + if percentage: + await self.async_set_percentage(percentage) + if preset_mode: + await self.async_set_preset_mode(preset_mode) else: result = await self._try_command( "Turning the miio device on failed.", self._device.on @@ -771,6 +829,22 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): class XiaomiAirPurifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Purifier.""" + PRESET_MODE_MAPPING = { + "Auto": AirpurifierOperationMode.Auto, + "Silent": AirpurifierOperationMode.Silent, + "Favorite": AirpurifierOperationMode.Favorite, + "Idle": AirpurifierOperationMode.Favorite, + } + + SPEED_MODE_MAPPING = { + 1: AirpurifierOperationMode.Silent, + 2: AirpurifierOperationMode.Medium, + 3: AirpurifierOperationMode.High, + 4: AirpurifierOperationMode.Strong, + } + + REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} + def __init__(self, name, device, entry, unique_id, allowed_failures=0): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) @@ -780,26 +854,60 @@ class XiaomiAirPurifier(XiaomiGenericDevice): if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO + # SUPPORT_SET_SPEED was disabled + # the device supports preset_modes only + self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO + self._supported_features = SUPPORT_PRESET_MODE + self._speed_count = 1 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 + # SUPPORT_SET_SPEED was disabled + # the device supports preset_modes only + self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7 + self._supported_features = SUPPORT_PRESET_MODE + self._speed_count = 1 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7 elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S + # SUPPORT_SET_SPEED was disabled + # the device supports preset_modes only + self._preset_modes = PRESET_MODES_AIRPURIFIER_2S + self._supported_features = SUPPORT_PRESET_MODE + self._speed_count = 1 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER_2S elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 + # SUPPORT_SET_SPEED was disabled + # the device supports preset_modes only + self._preset_modes = PRESET_MODES_AIRPURIFIER_3 + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE + self._speed_count = 3 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER_3 elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 + # SUPPORT_SET_SPEED was disabled + # the device supports preset_modes only + self._preset_modes = PRESET_MODES_AIRPURIFIER_V3 + self._supported_features = SUPPORT_PRESET_MODE + self._speed_count = 1 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 else: self._device_features = FEATURE_FLAGS_AIRPURIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER + self._preset_modes = PRESET_MODES_AIRPURIFIER + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE + self._speed_count = 4 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRPURIFIER self._state_attrs.update( @@ -846,10 +954,27 @@ class XiaomiAirPurifier(XiaomiGenericDevice): ) @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list + def preset_mode(self): + """Get the active preset mode.""" + if self._state: + preset_mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name + return preset_mode if preset_mode in self._preset_modes else None + return None + + @property + def percentage(self): + """Return the current percentage based speed.""" + if self._state: + mode = AirpurifierOperationMode(self._state_attrs[ATTR_MODE]) + if mode in self.REVERSE_SPEED_MODE_MAPPING: + return ranged_value_to_percentage( + (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] + ) + + return None + + # the speed attribute is deprecated, support will end with release 2021.7 @property def speed(self): """Return the current speed.""" @@ -858,6 +983,37 @@ class XiaomiAirPurifier(XiaomiGenericDevice): return None + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. + + This method is a coroutine. + """ + speed_mode = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) + ) + if speed_mode: + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + AirpurifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.PRESET_MODE_MAPPING[preset_mode], + ) + + # the async_set_speed function is deprecated, support will end with release 2021.7 + # it is added here only for compatibility with legacy speeds async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if self.supported_features & SUPPORT_SET_SPEED == 0: @@ -1004,6 +1160,34 @@ class XiaomiAirPurifier(XiaomiGenericDevice): class XiaomiAirPurifierMiot(XiaomiAirPurifier): """Representation of a Xiaomi Air Purifier (MiOT protocol).""" + PRESET_MODE_MAPPING = { + "Auto": AirpurifierMiotOperationMode.Auto, + "Silent": AirpurifierMiotOperationMode.Silent, + "Favorite": AirpurifierMiotOperationMode.Favorite, + "Fan": AirpurifierMiotOperationMode.Fan, + } + + @property + def percentage(self): + """Return the current percentage based speed.""" + if self._state: + fan_level = self._state_attrs[ATTR_FAN_LEVEL] + return ranged_value_to_percentage((1, 3), fan_level) + + return None + + @property + def preset_mode(self): + """Get the active preset mode.""" + if self._state: + preset_mode = AirpurifierMiotOperationMode( + self._state_attrs[ATTR_MODE] + ).name + return preset_mode if preset_mode in self._preset_modes else None + + return None + + # the speed attribute is deprecated, support will end with release 2021.7 @property def speed(self): """Return the current speed.""" @@ -1012,6 +1196,35 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): return None + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. + + This method is a coroutine. + """ + fan_level = math.ceil(percentage_to_ranged_value((1, 3), percentage)) + if fan_level: + await self._try_command( + "Setting fan level of the miio device failed.", + self._device.set_fan_level, + fan_level, + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.PRESET_MODE_MAPPING[preset_mode], + ) + + # the async_set_speed function is deprecated, support will end with release 2021.7 + # it is added here only for compatibility with legacy speeds async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if self.supported_features & SUPPORT_SET_SPEED == 0: @@ -1040,30 +1253,58 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirHumidifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Humidifier.""" + SPEED_MODE_MAPPING = { + 1: AirhumidifierOperationMode.Silent, + 2: AirhumidifierOperationMode.Medium, + 3: AirhumidifierOperationMode.High, + 4: AirhumidifierOperationMode.Strong, + } + + REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} + + PRESET_MODE_MAPPING = { + "Auto": AirhumidifierOperationMode.Auto, + } + def __init__(self, name, device, entry, unique_id): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) - + self._percentage = None + self._preset_mode = None + self._supported_features = SUPPORT_SET_SPEED + self._preset_modes = [] if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = [ mode.name for mode in AirhumidifierOperationMode if mode is not AirhumidifierOperationMode.Strong ] + self._supported_features |= SUPPORT_PRESET_MODE + self._preset_modes = PRESET_MODES_AIRHUMIDIFIER + self._speed_count = 3 elif self._model in [MODEL_AIRHUMIDIFIER_CA4]: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + self._supported_features |= SUPPORT_PRESET_MODE + self._preset_modes = PRESET_MODES_AIRHUMIDIFIER + self._speed_count = 3 else: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = [ mode.name for mode in AirhumidifierOperationMode if mode is not AirhumidifierOperationMode.Auto ] + self._supported_features |= SUPPORT_PRESET_MODE + self._preset_modes = PRESET_MODES_AIRHUMIDIFIER + self._speed_count = 4 self._state_attrs.update( {attribute: None for attribute in self._available_attributes} @@ -1095,10 +1336,27 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): _LOGGER.error("Got exception while fetching the state: %s", ex) @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list + def preset_mode(self): + """Get the active preset mode.""" + if self._state: + preset_mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name + return preset_mode if preset_mode in self._preset_modes else None + return None + + @property + def percentage(self): + """Return the current percentage based speed.""" + if self._state: + mode = AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]) + if mode in self.REVERSE_SPEED_MODE_MAPPING: + return ranged_value_to_percentage( + (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] + ) + + return None + + # the speed attribute is deprecated, support will end with release 2021.7 @property def speed(self): """Return the current speed.""" @@ -1107,6 +1365,37 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): return None + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. + + This method is a coroutine. + """ + speed_mode = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) + ) + if speed_mode: + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + AirhumidifierOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.PRESET_MODE_MAPPING[preset_mode], + ) + + # the async_set_speed function is deprecated, support will end with release 2021.7 + # it is added here only for compatibility with legacy speeds async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if self.supported_features & SUPPORT_SET_SPEED == 0: @@ -1168,21 +1457,64 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): """Representation of a Xiaomi Air Humidifier (MiOT protocol).""" - MODE_MAPPING = { + PRESET_MODE_MAPPING = { + AirhumidifierMiotOperationMode.Auto: "Auto", + } + + REVERSE_PRESET_MODE_MAPPING = {v: k for k, v in PRESET_MODE_MAPPING.items()} + + SPEED_MAPPING = { AirhumidifierMiotOperationMode.Low: SPEED_LOW, AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM, AirhumidifierMiotOperationMode.High: SPEED_HIGH, } - REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()} + REVERSE_SPEED_MAPPING = {v: k for k, v in SPEED_MAPPING.items()} + SPEEDS = [ + AirhumidifierMiotOperationMode.Low, + AirhumidifierMiotOperationMode.Mid, + AirhumidifierMiotOperationMode.High, + ] + + # the speed attribute is deprecated, support will end with release 2021.7 + # it is added here for compatibility @property def speed(self): - """Return the current speed.""" + """Return current legacy speed.""" + if ( + self.state + and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) + in self.SPEED_MAPPING + ): + return self.SPEED_MAPPING[ + AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) + ] + return None + + @property + def percentage(self): + """Return the current percentage based speed.""" + if ( + self.state + and AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) + in self.SPEEDS + ): + return ranged_value_to_percentage( + (1, self.speed_count), self._state_attrs[ATTR_MODE] + ) + + return None + + @property + def preset_mode(self): + """Return the current preset_mode.""" if self._state: - return self.MODE_MAPPING.get( + mode = self.PRESET_MODE_MAPPING.get( AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE]) ) + if mode in self._preset_modes: + return mode return None @@ -1196,12 +1528,42 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): return None + # the async_set_speed function is deprecated, support will end with release 2021.7 + # it is added here only for compatibility with legacy speeds async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" + """Override for set async_set_speed of the super() class.""" + if speed and speed in self.REVERSE_SPEED_MAPPING: + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.REVERSE_SPEED_MAPPING[speed], + ) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. + + This method is a coroutine. + """ + mode = math.ceil(percentage_to_ranged_value((1, 3), percentage)) + if mode: + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + AirhumidifierMiotOperationMode(mode), + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - self.REVERSE_MODE_MAPPING[speed], + self.REVERSE_PRESET_MODE_MAPPING[preset_mode], ) async def async_set_led_brightness(self, brightness: int = 2): @@ -1230,13 +1592,30 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" + SPEED_MODE_MAPPING = { + 1: AirfreshOperationMode.Silent, + 2: AirfreshOperationMode.Low, + 3: AirfreshOperationMode.Middle, + 4: AirfreshOperationMode.Strong, + } + + REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} + + PRESET_MODE_MAPPING = { + "Auto": AirfreshOperationMode.Auto, + "Interval": AirfreshOperationMode.Interval, + } + def __init__(self, name, device, entry, unique_id): """Initialize the miio device.""" super().__init__(name, device, entry, unique_id) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH + # the speed_list attribute is deprecated, support will end with release 2021.7 self._speed_list = OPERATION_MODES_AIRFRESH + self._speed_count = 4 + self._preset_modes = PRESET_MODES_AIRFRESH self._state_attrs.update( {attribute: None for attribute in self._available_attributes} ) @@ -1267,10 +1646,27 @@ class XiaomiAirFresh(XiaomiGenericDevice): _LOGGER.error("Got exception while fetching the state: %s", ex) @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return self._speed_list + def preset_mode(self): + """Get the active preset mode.""" + if self._state: + preset_mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name + return preset_mode if preset_mode in self._preset_modes else None + return None + + @property + def percentage(self): + """Return the current percentage based speed.""" + if self._state: + mode = AirfreshOperationMode(self._state_attrs[ATTR_MODE]) + if mode in self.REVERSE_SPEED_MODE_MAPPING: + return ranged_value_to_percentage( + (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] + ) + + return None + + # the speed attribute is deprecated, support will end with release 2021.7 @property def speed(self): """Return the current speed.""" @@ -1279,6 +1675,37 @@ class XiaomiAirFresh(XiaomiGenericDevice): return None + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. + + This method is a coroutine. + """ + speed_mode = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) + ) + if speed_mode: + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + AirfreshOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.PRESET_MODE_MAPPING[preset_mode], + ) + + # the async_set_speed function is deprecated, support will end with release 2021.7 + # it is added here only for compatibility with legacy speeds async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" if self.supported_features & SUPPORT_SET_SPEED == 0: From 33e2b910c22eaa482732c43309521fa2dd80422c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jun 2021 23:10:58 +1200 Subject: [PATCH 473/750] Add @jesserockz to ESPHome codeowners (#52115) --- CODEOWNERS | 2 +- homeassistant/components/esphome/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c19f4cbc721..891f0e37f72 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -147,7 +147,7 @@ homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti -homeassistant/components/esphome/* @OttoWinter +homeassistant/components/esphome/* @OttoWinter @jesserockz homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @RenierM26 @baqs diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b732713335e..e5992c84358 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": ["aioesphomeapi==2.9.0"], "zeroconf": ["_esphomelib._tcp.local."], - "codeowners": ["@OttoWinter"], + "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push" } From b3b23066a8cb6d68e136064c97046bbc0e334c9c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 13:11:33 +0200 Subject: [PATCH 474/750] Add state class to Huisbaasje (#52114) --- homeassistant/components/huisbaasje/const.py | 7 +++++++ homeassistant/components/huisbaasje/sensor.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index abac03e6182..f2565a15ce2 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -8,6 +8,7 @@ from huisbaasje.const import ( SOURCE_TYPE_GAS, ) +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -48,26 +49,31 @@ SENSORS_INFO = [ "name": "Huisbaasje Current Power", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Current Power In", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Current Power In Low", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Current Power Out", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Current Power Out Low", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Energy Today", @@ -107,6 +113,7 @@ SENSORS_INFO = [ "source_type": SOURCE_TYPE_GAS, "icon": "mdi:fire", "precision": 1, + "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Gas Today", diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 1ea392b8269..038325ece4a 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -1,4 +1,6 @@ """Platform for sensor integration.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, POWER_WATT @@ -38,6 +40,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", precision: int = 0, + state_class: str | None = None, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -49,6 +52,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): self._sensor_type = sensor_type self._icon = icon self._precision = precision + self._attr_state_class = state_class @property def unique_id(self) -> str: From 5a4a1a250d88889f98a9abbdca29173e2bb3e925 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 23 Jun 2021 14:56:20 +0200 Subject: [PATCH 475/750] Catch exception for failed webhook drop for netatmo (#52119) --- homeassistant/components/netatmo/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 8158c23742f..d92e50107c9 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -131,7 +131,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: {"type": "None", "data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}}, ) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() + try: + await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() + except pyatmo.ApiError: + _LOGGER.debug( + "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID] + ) async def register_webhook(event): if CONF_WEBHOOK_ID not in entry.data: From a374e248432bd820496d2943404514b960aa2e34 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Jun 2021 15:32:25 +0200 Subject: [PATCH 476/750] Add monetary sensor device class (#52087) * Add total_cost sensor device class * Change to DEVICE_CLASS_MONETARY --- homeassistant/components/sensor/__init__.py | 2 ++ homeassistant/components/sensor/recorder.py | 2 ++ homeassistant/const.py | 11 ++++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2857a3cc5c2..8fac4e50b3e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, @@ -52,6 +53,7 @@ DEVICE_CLASSES: Final[list[str]] = [ DEVICE_CLASS_ENERGY, # energy (kWh, Wh) DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) + DEVICE_CLASS_MONETARY, # Amount of money (currency) DEVICE_CLASS_SIGNAL_STRENGTH, # signal strength (dB/dBm) DEVICE_CLASS_TEMPERATURE, # temperature (C/F) DEVICE_CLASS_TIMESTAMP, # timestamp (ISO8601) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index fb6c8d2fba3..6de5ee6338a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -26,6 +27,7 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, + DEVICE_CLASS_MONETARY: {"sum"}, } diff --git a/homeassistant/const.py b/homeassistant/const.py index 529b0969e4a..a25a5ec0908 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -235,16 +235,17 @@ EVENT_TIME_CHANGED: Final = "time_changed" DEVICE_CLASS_BATTERY: Final = "battery" DEVICE_CLASS_CO: Final = "carbon_monoxide" DEVICE_CLASS_CO2: Final = "carbon_dioxide" +DEVICE_CLASS_CURRENT: Final = "current" +DEVICE_CLASS_ENERGY: Final = "energy" DEVICE_CLASS_HUMIDITY: Final = "humidity" DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" +DEVICE_CLASS_MONETARY: Final = "monetary" +DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" +DEVICE_CLASS_POWER: Final = "power" +DEVICE_CLASS_PRESSURE: Final = "pressure" DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" DEVICE_CLASS_TEMPERATURE: Final = "temperature" DEVICE_CLASS_TIMESTAMP: Final = "timestamp" -DEVICE_CLASS_PRESSURE: Final = "pressure" -DEVICE_CLASS_POWER: Final = "power" -DEVICE_CLASS_CURRENT: Final = "current" -DEVICE_CLASS_ENERGY: Final = "energy" -DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" DEVICE_CLASS_VOLTAGE: Final = "voltage" # #### STATES #### From 77de233679e88365b28fbc42b434cb373bbb4ad6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Jun 2021 15:35:18 +0200 Subject: [PATCH 477/750] Update MQTT number to treat received payload as UTF-8 (#52121) * Update MQTT number to treat received payload as UTF-8 * Lint --- homeassistant/components/mqtt/number.py | 17 +++++++++-------- tests/components/mqtt/test_number.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 95409924fa4..ede9adad51f 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -44,7 +44,7 @@ DEFAULT_OPTIMISTIC = False def validate_config(config): """Validate that the configuration is valid, throws if it isn't.""" if config.get(CONF_MIN) >= config.get(CONF_MAX): - raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'") + raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'") return config @@ -94,10 +94,10 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Number.""" self._config = config + self._optimistic = False self._sub_state = None self._current_number = None - self._optimistic = config.get(CONF_OPTIMISTIC) NumberEntity.__init__(self) MqttEntity.__init__(self, None, config, config_entry, discovery_data) @@ -107,6 +107,10 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """Return the config schema.""" return PLATFORM_SCHEMA + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._optimistic = config[CONF_OPTIMISTIC] + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -114,16 +118,14 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" + payload = msg.payload try: - if msg.payload.decode("utf-8").isnumeric(): + if payload.isnumeric(): num_value = int(msg.payload) else: num_value = float(msg.payload) except ValueError: - _LOGGER.warning( - "Payload '%s' is not a Number", - msg.payload.decode("utf-8", errors="ignore"), - ) + _LOGGER.warning("Payload '%s' is not a Number", msg.payload) return if num_value < self.min_value or num_value > self.max_value: @@ -151,7 +153,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): "topic": self._config.get(CONF_STATE_TOPIC), "msg_callback": message_received, "qos": self._config[CONF_QOS], - "encoding": None, } }, ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index d93b0483865..e9dca6f6a5e 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -359,7 +359,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1" + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload="1" ) @@ -408,7 +408,7 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): ) await hass.async_block_till_done() - assert f"'{CONF_MAX}'' must be > '{CONF_MIN}'" in caplog.text + assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): From 80ae346318654783763ec81e9a0c22c418c2ee64 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Jun 2021 15:44:18 +0200 Subject: [PATCH 478/750] Pass the hass object to all MQTT component constructors (#52124) --- homeassistant/components/mqtt/camera.py | 12 ++++++------ homeassistant/components/mqtt/light/schema_json.py | 6 +++--- .../components/mqtt/light/schema_template.py | 6 +++--- homeassistant/components/mqtt/vacuum/__init__.py | 8 ++++---- .../components/mqtt/vacuum/schema_legacy.py | 8 ++++---- homeassistant/components/mqtt/vacuum/schema_state.py | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 0a9f37ac9ea..1db9faf9058 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -32,33 +32,33 @@ async def async_setup_platform( ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(async_add_entities, config) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT camera dynamically through MQTT discovery.""" setup = functools.partial( - _async_setup_entity, async_add_entities, config_entry=config_entry + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) await async_setup_entry_helper(hass, camera.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - async_add_entities, config, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Camera.""" - async_add_entities([MqttCamera(config, config_entry, discovery_data)]) + async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT Camera.""" self._last_image = None Camera.__init__(self) - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 5143b92622a..0c8080db5d2 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -151,13 +151,13 @@ async def async_setup_entity_json( hass, config: ConfigType, async_add_entities, config_entry, discovery_data ): """Set up a MQTT JSON Light.""" - async_add_entities([MqttLightJson(config, config_entry, discovery_data)]) + async_add_entities([MqttLightJson(hass, config, config_entry, discovery_data)]) class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" self._state = False self._supported_features = 0 @@ -176,7 +176,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._white_value = None self._xy = None - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index c5eee7006d6..d2df37828f1 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -87,13 +87,13 @@ async def async_setup_entity_template( hass, config, async_add_entities, config_entry, discovery_data ): """Set up a MQTT Template light.""" - async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)]) + async_add_entities([MqttLightTemplate(hass, config, config_entry, discovery_data)]) class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" self._state = False @@ -108,7 +108,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): self._hs = None self._effect = None - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 85fd1247381..12d2ff5319c 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -27,22 +27,22 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(async_add_entities, config) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT vacuum dynamically through MQTT discovery.""" setup = functools.partial( - _async_setup_entity, async_add_entities, config_entry=config_entry + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) await async_setup_entry_helper(hass, DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - async_add_entities, config, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT vacuum.""" setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} await setup_entity[config[CONF_SCHEMA]]( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index f0f00a72bb4..87044c46cfb 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -151,16 +151,16 @@ PLATFORM_SCHEMA_LEGACY = ( async def async_setup_entity_legacy( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ): """Set up a MQTT Vacuum Legacy.""" - async_add_entities([MqttVacuum(config, config_entry, discovery_data)]) + async_add_entities([MqttVacuum(hass, config, config_entry, discovery_data)]) class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" self._cleaning = False self._charging = False @@ -171,7 +171,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): self._fan_speed = "unknown" self._fan_speed_list = [] - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 37a12d33df6..f35a036ec10 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -135,22 +135,22 @@ PLATFORM_SCHEMA_STATE = ( async def async_setup_entity_state( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ): """Set up a State MQTT Vacuum.""" - async_add_entities([MqttStateVacuum(config, config_entry, discovery_data)]) + async_add_entities([MqttStateVacuum(hass, config, config_entry, discovery_data)]) class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" self._state = None self._state_attrs = {} self._fan_speed_list = [] - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): From 75faee4f257e9f60785c735a87e3adfba091c9eb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 23 Jun 2021 15:46:28 +0200 Subject: [PATCH 479/750] Use attrs instead of properties in Bravia TV integration (#52045) * Use attrs instead of properties * Revert to using properties for dynamic data * Move volume_level to coordinator * Move media_title to coordinator * Remove unused variables * Fix variable name * Revert removed variables --- homeassistant/components/braviatv/__init__.py | 15 ++++++--- .../components/braviatv/media_player.py | 32 +++---------------- homeassistant/components/braviatv/remote.py | 21 ++---------- 3 files changed, 19 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index a29d899cb30..eecf8533800 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -71,9 +71,9 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.pin = pin self.ignored_sources = ignored_sources self.muted = False - self.program_name = None self.channel_name = None self.channel_number = None + self.media_title = None self.source = None self.source_list = [] self.original_content_list = [] @@ -85,7 +85,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.audio_output = None self.min_volume = None self.max_volume = None - self.volume = None + self.volume_level = None self.is_on = False # Assume that the TV is in Play mode self.playing = True @@ -117,8 +117,9 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Refresh volume information.""" volume_info = self.braviarc.get_volume_info(self.audio_output) if volume_info is not None: + volume = volume_info.get("volume") + self.volume_level = volume / 100 if volume is not None else None self.audio_output = volume_info.get("target") - self.volume = volume_info.get("volume") self.min_volume = volume_info.get("minVolume") self.max_volume = volume_info.get("maxVolume") self.muted = volume_info.get("mute") @@ -140,7 +141,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): def _refresh_playing_info(self): """Refresh playing information.""" playing_info = self.braviarc.get_playing_info() - self.program_name = playing_info.get("programTitle") + program_name = playing_info.get("programTitle") self.channel_name = playing_info.get("title") self.program_media_type = playing_info.get("programMediaType") self.channel_number = playing_info.get("dispNum") @@ -150,6 +151,12 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.start_date_time = playing_info.get("startDateTime") if not playing_info: self.channel_name = "App" + if self.channel_name is not None: + self.media_title = self.channel_name + if program_name is not None: + self.media_title = f"{self.media_title}: {program_name}" + else: + self.media_title = None def _update_tv_data(self): """Connect and update TV info.""" diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 7009b04cba7..2773a185bb1 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -116,27 +116,12 @@ class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def __init__(self, coordinator, name, unique_id, device_info): """Initialize the entity.""" - self._name = name - self._unique_id = unique_id - self._device_info = device_info + self._attr_device_info = device_info + self._attr_name = name + self._attr_unique_id = unique_id super().__init__(coordinator) - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - return self._device_info - @property def state(self): """Return the state of the device.""" @@ -157,9 +142,7 @@ class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def volume_level(self): """Volume level of the media player (0..1).""" - if self.coordinator.volume is not None: - return self.coordinator.volume / 100 - return None + return self.coordinator.volume_level @property def is_volume_muted(self): @@ -169,12 +152,7 @@ class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def media_title(self): """Title of current playing media.""" - return_value = None - if self.coordinator.channel_name is not None: - return_value = self.coordinator.channel_name - if self.coordinator.program_name is not None: - return_value = f"{return_value}: {self.coordinator.program_name}" - return return_value + return self.coordinator.media_title @property def media_content_id(self): diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 8bd5fb09af3..613d67f0187 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -29,27 +29,12 @@ class BraviaTVRemote(CoordinatorEntity, RemoteEntity): def __init__(self, coordinator, name, unique_id, device_info): """Initialize the entity.""" - self._name = name - self._unique_id = unique_id - self._device_info = device_info + self._attr_device_info = device_info + self._attr_name = name + self._attr_unique_id = unique_id super().__init__(coordinator) - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._unique_id - - @property - def device_info(self): - """Return device specific attributes.""" - return self._device_info - - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def is_on(self): """Return true if device is on.""" From db5bf8ab23b42be80ddf17fd78cf267990674f8f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 23 Jun 2021 15:51:27 +0200 Subject: [PATCH 480/750] Bump pyatmo version (#52112) * Bump pyatmo version * Update tests --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/netatmo/common.py | 14 +++++++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 60a54df8a6e..a6630a00f50 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==5.0.1" + "pyatmo==5.1.0" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 1fe6fa8f2ce..887cef869d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1306,7 +1306,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.0.1 +pyatmo==5.1.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 175c1fc2d77..9e30b7819f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -731,7 +731,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.0.1 +pyatmo==5.1.0 # homeassistant.components.apple_tv pyatv==0.7.7 diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 32202cb85e5..5ba989e2504 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -7,6 +7,7 @@ from homeassistant.components.webhook import async_handle_webhook from homeassistant.util.aiohttp import MockRequest from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMockResponse CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -50,7 +51,7 @@ async def fake_post_request(*args, **kwargs): if endpoint in "snapshot_720.jpg": return b"test stream image bytes" - if endpoint in [ + elif endpoint in [ "setpersonsaway", "setpersonshome", "setstate", @@ -58,9 +59,16 @@ async def fake_post_request(*args, **kwargs): "setthermmode", "switchhomeschedule", ]: - return f'{{"{endpoint}": true}}' + payload = f'{{"{endpoint}": true}}' - return json.loads(load_fixture(f"netatmo/{endpoint}.json")) + else: + payload = json.loads(load_fixture(f"netatmo/{endpoint}.json")) + + return AiohttpClientMockResponse( + method="POST", + url=kwargs["url"], + json=payload, + ) async def fake_post_request_no_data(*args, **kwargs): From 2351f2d95efeb1a0af37080ae418ffe08bb70e9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Jun 2021 15:53:17 +0200 Subject: [PATCH 481/750] Warn when receiving message on illegal MQTT discovery topic (#52106) * Warn when receiving message on illegal MQTT discovery topic * Fix test --- homeassistant/components/mqtt/discovery.py | 4 ++++ tests/components/mqtt/test_discovery.py | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e68b47abe02..e4f461324a9 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -95,6 +95,10 @@ async def async_start( # noqa: C901 match = TOPIC_MATCHER.match(topic_trimmed) if not match: + if topic_trimmed.endswith("config"): + _LOGGER.warning( + "Received message on illegal discovery topic '%s'", topic + ) return component, node_id, object_id = match.groups() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d55c8e0eccc..907c3d398b6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -55,18 +55,30 @@ async def test_subscribing_config_topic(hass, mqtt_mock): assert discovery_topic + "/+/+/+/config" in topics -async def test_invalid_topic(hass, mqtt_mock): +@pytest.mark.parametrize( + "topic, log", + [ + ("homeassistant/binary_sensor/bla/not_config", False), + ("homeassistant/binary_sensor/rörkrökare/config", True), + ], +) +async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): """Test sending to invalid topic.""" with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: mock_dispatcher_send = AsyncMock(return_value=None) - async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/not_config", "{}" - ) + async_fire_mqtt_message(hass, topic, "{}") await hass.async_block_till_done() assert not mock_dispatcher_send.called + if log: + assert ( + f"Received message on illegal discovery topic '{topic}'" in caplog.text + ) + else: + assert "Received message on illegal discovery topic'" not in caplog.text + caplog.clear() async def test_invalid_json(hass, mqtt_mock, caplog): From 4e88b44286a5d9392c7776bfa1954f9204c909e9 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 23 Jun 2021 09:10:29 -0500 Subject: [PATCH 482/750] Use attrs instead of properties for directv (#51918) * Use attrs instead of properties for directv * Update __init__.py * Create entity.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update entity.py * Update __init__.py * Update media_player.py * Update remote.py * Update media_player.py * Update media_player.py --- homeassistant/components/directv/__init__.py | 41 +------------ homeassistant/components/directv/entity.py | 39 ++++++++++++ .../components/directv/media_player.py | 61 ++++++------------- homeassistant/components/directv/remote.py | 35 +++-------- 4 files changed, 68 insertions(+), 108 deletions(-) create mode 100644 homeassistant/components/directv/entity.py diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index b79a55394d5..2fec28db14a 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -6,21 +6,13 @@ from datetime import timedelta from directv import DIRECTV, DIRECTVError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - ATTR_VIA_DEVICE, - DOMAIN, -) +from .const import DOMAIN CONFIG_SCHEMA = cv.deprecated(DOMAIN) @@ -52,32 +44,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class DIRECTVEntity(Entity): - """Defines a base DirecTV entity.""" - - def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: - """Initialize the DirecTV entity.""" - self._address = address - self._device_id = address if address != "0" else dtv.device.info.receiver_id - self._is_client = address != "0" - self._name = name - self.dtv = dtv - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this DirecTV receiver.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.name, - ATTR_MANUFACTURER: self.dtv.device.info.brand, - ATTR_MODEL: None, - ATTR_SOFTWARE_VERSION: self.dtv.device.info.version, - ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id), - } diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py new file mode 100644 index 00000000000..c632ad7e84c --- /dev/null +++ b/homeassistant/components/directv/entity.py @@ -0,0 +1,39 @@ +"""Base DirecTV Entity.""" +from __future__ import annotations + +from directv import DIRECTV + +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + ATTR_VIA_DEVICE, + DOMAIN, +) + + +class DIRECTVEntity(Entity): + """Defines a base DirecTV entity.""" + + def __init__(self, *, dtv: DIRECTV, address: str = "0") -> None: + """Initialize the DirecTV entity.""" + self._address = address + self._device_id = address if address != "0" else dtv.device.info.receiver_id + self._is_client = address != "0" + self.dtv = dtv + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this DirecTV receiver.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: self.name, + ATTR_MANUFACTURER: self.dtv.device.info.brand, + ATTR_MODEL: None, + ATTR_SOFTWARE_VERSION: self.dtv.device.info.version, + ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id), + } diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 5d7f7d1185b..1a7d07c5ebd 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -29,7 +29,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from . import DIRECTVEntity from .const import ( ATTR_MEDIA_CURRENTLY_RECORDING, ATTR_MEDIA_RATING, @@ -37,6 +36,7 @@ from .const import ( ATTR_MEDIA_START_TIME, DOMAIN, ) +from .entity import DIRECTVEntity _LOGGER = logging.getLogger(__name__) @@ -91,12 +91,15 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): """Initialize DirecTV media player.""" super().__init__( dtv=dtv, - name=name, address=address, ) - self._assumed_state = None - self._available = False + self._attr_unique_id = self._device_id + self._attr_name = name + self._attr_device_class = DEVICE_CLASS_RECEIVER + self._attr_available = False + self._attr_assumed_state = None + self._is_recorded = None self._is_standby = True self._last_position = None @@ -108,12 +111,12 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): async def async_update(self): """Retrieve latest state.""" self._state = await self.dtv.state(self._address) - self._available = self._state.available + self._attr_available = self._state.available self._is_standby = self._state.standby self._program = self._state.program if self._is_standby: - self._assumed_state = False + self._attr_assumed_state = False self._is_recorded = None self._last_position = None self._last_update = None @@ -123,7 +126,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._is_recorded = self._program.recorded self._last_position = self._program.position self._last_update = self._state.at - self._assumed_state = self._is_recorded + self._attr_assumed_state = self._is_recorded @property def extra_state_attributes(self): @@ -137,24 +140,6 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): ATTR_MEDIA_START_TIME: self.media_start_time, } - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def device_class(self) -> str | None: - """Return the class of this device.""" - return DEVICE_CLASS_RECEIVER - - @property - def unique_id(self): - """Return a unique ID to use for this media player.""" - if self._address == "0": - return self.dtv.device.info.receiver_id - - return self._address - # MediaPlayerEntity properties and methods @property def state(self): @@ -170,16 +155,6 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return STATE_PLAYING - @property - def available(self): - """Return if able to retrieve information from DVR or not.""" - return self._available - - @property - def assumed_state(self): - """Return if we assume the state or not.""" - return self._assumed_state - @property def media_content_id(self): """Return the content ID of current playing media.""" @@ -316,7 +291,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._is_client: raise NotImplementedError() - _LOGGER.debug("Turn on %s", self._name) + _LOGGER.debug("Turn on %s", self.name) await self.dtv.remote("poweron", self._address) async def async_turn_off(self): @@ -324,32 +299,32 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._is_client: raise NotImplementedError() - _LOGGER.debug("Turn off %s", self._name) + _LOGGER.debug("Turn off %s", self.name) await self.dtv.remote("poweroff", self._address) async def async_media_play(self): """Send play command.""" - _LOGGER.debug("Play on %s", self._name) + _LOGGER.debug("Play on %s", self.name) await self.dtv.remote("play", self._address) async def async_media_pause(self): """Send pause command.""" - _LOGGER.debug("Pause on %s", self._name) + _LOGGER.debug("Pause on %s", self.name) await self.dtv.remote("pause", self._address) async def async_media_stop(self): """Send stop command.""" - _LOGGER.debug("Stop on %s", self._name) + _LOGGER.debug("Stop on %s", self.name) await self.dtv.remote("stop", self._address) async def async_media_previous_track(self): """Send rewind command.""" - _LOGGER.debug("Rewind on %s", self._name) + _LOGGER.debug("Rewind on %s", self.name) await self.dtv.remote("rew", self._address) async def async_media_next_track(self): """Send fast forward command.""" - _LOGGER.debug("Fast forward on %s", self._name) + _LOGGER.debug("Fast forward on %s", self.name) await self.dtv.remote("ffwd", self._address) async def async_play_media(self, media_type, media_id, **kwargs): @@ -362,5 +337,5 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): ) return - _LOGGER.debug("Changing channel on %s to %s", self._name, media_id) + _LOGGER.debug("Changing channel on %s to %s", self.name, media_id) await self.dtv.tune(media_id, self._address) diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index 424b5ba4ec6..52e94bc2608 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -13,8 +13,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DIRECTVEntity from .const import DOMAIN +from .entity import DIRECTVEntity _LOGGER = logging.getLogger(__name__) @@ -49,41 +49,24 @@ class DIRECTVRemote(DIRECTVEntity, RemoteEntity): """Initialize DirecTV remote.""" super().__init__( dtv=dtv, - name=name, address=address, ) - self._available = False - self._is_on = True - - @property - def available(self): - """Return if able to retrieve information from device or not.""" - return self._available - - @property - def unique_id(self): - """Return a unique ID.""" - if self._address == "0": - return self.dtv.device.info.receiver_id - - return self._address - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._is_on + self._attr_unique_id = self._device_id + self._attr_name = name + self._attr_available = False + self._attr_is_on = True async def async_update(self) -> None: """Update device state.""" status = await self.dtv.status(self._address) if status in ("active", "standby"): - self._available = True - self._is_on = status == "active" + self._attr_available = True + self._attr_is_on = status == "active" else: - self._available = False - self._is_on = False + self._attr_available = False + self._attr_is_on = False async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" From 742159a6a62e8142a5b84c0cc09e5ad1f95c2ea6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 23 Jun 2021 17:20:49 +0200 Subject: [PATCH 483/750] Add number entity to KNX (#51786) Co-authored-by: Franck Nijhof --- homeassistant/components/knx/__init__.py | 2 + homeassistant/components/knx/const.py | 10 ++- homeassistant/components/knx/number.py | 96 ++++++++++++++++++++++++ homeassistant/components/knx/schema.py | 75 +++++++++++++++++- 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/knx/number.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c30b24475c0..613426aaa8f 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -49,6 +49,7 @@ from .schema import ( FanSchema, LightSchema, NotifySchema, + NumberSchema, SceneSchema, SensorSchema, SwitchSchema, @@ -93,6 +94,7 @@ CONFIG_SCHEMA = vol.Schema( **FanSchema.platform_node(), **LightSchema.platform_node(), **NotifySchema.platform_node(), + **NumberSchema.platform_node(), **SceneSchema.platform_node(), **SensorSchema.platform_node(), **SwitchSchema.platform_node(), diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 74c6045b767..08d431b0cff 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -26,14 +26,15 @@ DOMAIN: Final = "knx" # Address is used for configuration and services by the same functions so the key has to match KNX_ADDRESS: Final = "address" -CONF_KNX_ROUTING: Final = "routing" -CONF_KNX_TUNNELING: Final = "tunneling" -CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" CONF_INVERT: Final = "invert" CONF_KNX_EXPOSE: Final = "expose" +CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" +CONF_KNX_ROUTING: Final = "routing" +CONF_KNX_TUNNELING: Final = "tunneling" +CONF_RESET_AFTER: Final = "reset_after" +CONF_RESPOND_TO_READ = "respond_to_read" CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" -CONF_RESET_AFTER: Final = "reset_after" ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" @@ -56,6 +57,7 @@ class SupportedPlatforms(Enum): FAN = "fan" LIGHT = "light" NOTIFY = "notify" + NUMBER = "number" SCENE = "scene" SENSOR = "sensor" SWITCH = "switch" diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py new file mode 100644 index 00000000000..b438551ebda --- /dev/null +++ b/homeassistant/components/knx/number.py @@ -0,0 +1,96 @@ +"""Support for KNX/IP numeric values.""" +from __future__ import annotations + +from typing import cast + +from xknx import XKNX +from xknx.devices import NumericValue + +from homeassistant.components.number import NumberEntity +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, DOMAIN, KNX_ADDRESS +from .knx_entity import KnxEntity +from .schema import NumberSchema + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up number entities for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + + async_add_entities( + KNXNumber(xknx, entity_config) for entity_config in platform_config + ) + + +def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: + """Return a KNX NumericValue to be used within XKNX.""" + return NumericValue( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + value_type=config[CONF_TYPE], + ) + + +class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): + """Representation of a KNX number.""" + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX number.""" + self._device: NumericValue + super().__init__(_create_numeric_value(xknx, config)) + self._unique_id = f"{self._device.sensor_value.group_address}" + + self._attr_min_value = config.get( + NumberSchema.CONF_MIN, + self._device.sensor_value.dpt_class.value_min, + ) + self._attr_max_value = config.get( + NumberSchema.CONF_MAX, + self._device.sensor_value.dpt_class.value_max, + ) + self._attr_step = config.get( + NumberSchema.CONF_STEP, + self._device.sensor_value.dpt_class.resolution, + ) + self._device.sensor_value.value = max(0, self.min_value) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if not self._device.sensor_value.readable and ( + last_state := await self.async_get_last_state() + ): + if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._device.sensor_value.value = float(last_state.state) + + @property + def value(self) -> float: + """Return the entity value to represent the entity state.""" + # self._device.sensor_value.value is set in __init__ so it is never None + return cast(float, self._device.resolve_state()) + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + if value < self.min_value or value > self.max_value: + raise ValueError( + f"Invalid value for {self.entity_id}: {value} " + f"(range {self.min_value} - {self.max_value})" + ) + await self._device.set(value) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 0715b68d575..37730010104 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -2,12 +2,13 @@ from __future__ import annotations from abc import ABC +from collections import OrderedDict from typing import Any, ClassVar import voluptuous as vol from xknx import XKNX from xknx.devices.climate import SetpointShiftMode -from xknx.dpt import DPTBase +from xknx.dpt import DPTBase, DPTNumeric from xknx.exceptions import CouldNotParseAddress from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.telegram.address import IndividualAddress, parse_device_group_address @@ -33,6 +34,7 @@ from .const import ( CONF_KNX_ROUTING, CONF_KNX_TUNNELING, CONF_RESET_AFTER, + CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, CONF_SYNC_STATE, CONTROLLER_MODES, @@ -70,6 +72,50 @@ ia_validator = vol.Any( ) +def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict: + """Validate a number entity configurations dependent on configured value type.""" + value_type = entity_config[CONF_TYPE] + min_config: float | None = entity_config.get(NumberSchema.CONF_MIN) + max_config: float | None = entity_config.get(NumberSchema.CONF_MAX) + step_config: float | None = entity_config.get(NumberSchema.CONF_STEP) + dpt_class = DPTNumeric.parse_transcoder(value_type) + + if dpt_class is None: + raise vol.Invalid(f"'type: {value_type}' is not a valid numeric sensor type.") + # Inifinity is not supported by Home Assistant frontend so user defined + # config is required if if xknx DPTNumeric subclass defines it as limit. + if min_config is None and dpt_class.value_min == float("-inf"): + raise vol.Invalid(f"'min' key required for value type '{value_type}'") + if min_config is not None and min_config < dpt_class.value_min: + raise vol.Invalid( + f"'min: {min_config}' undercuts possible minimum" + f" of value type '{value_type}': {dpt_class.value_min}" + ) + + if max_config is None and dpt_class.value_max == float("inf"): + raise vol.Invalid(f"'max' key required for value type '{value_type}'") + if max_config is not None and max_config > dpt_class.value_max: + raise vol.Invalid( + f"'max: {max_config}' exceeds possible maximum" + f" of value type '{value_type}': {dpt_class.value_max}" + ) + + if step_config is not None and step_config < dpt_class.resolution: + raise vol.Invalid( + f"'step: {step_config}' undercuts possible minimum step" + f" of value type '{value_type}': {dpt_class.resolution}" + ) + + return entity_config + + +def numeric_type_validator(value: Any) -> str | int: + """Validate that value is parsable as numeric sensor type.""" + if isinstance(value, (str, int)) and DPTNumeric.parse_transcoder(value) is not None: + return value + raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.") + + def sensor_type_validator(value: Any) -> str | int: """Validate that value is parsable as sensor type.""" if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None: @@ -525,6 +571,33 @@ class NotifySchema(KNXPlatformSchema): ) +class NumberSchema(KNXPlatformSchema): + """Voluptuous schema for KNX numbers.""" + + PLATFORM_NAME = SupportedPlatforms.NUMBER.value + + CONF_MAX = "max" + CONF_MIN = "min" + CONF_STEP = "step" + DEFAULT_NAME = "KNX Number" + + ENTITY_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Required(CONF_TYPE): numeric_type_validator, + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_MIN): vol.Coerce(float), + vol.Optional(CONF_STEP): cv.positive_float, + } + ), + number_limit_sub_validator, + ) + + class SceneSchema(KNXPlatformSchema): """Voluptuous schema for KNX scenes.""" From ed4a3d275a6c1ef7f03e6ae35a08a589e1ca0c3f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 23 Jun 2021 18:19:45 +0200 Subject: [PATCH 484/750] Fix ezviz options flow test patch (#52125) --- tests/components/ezviz/test_config_flow.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 2775781cf00..fe3c271b390 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -238,29 +238,28 @@ async def test_async_step_discovery( async def test_options_flow(hass): """Test updating options.""" - with patch("homeassistant.components.ezviz.PLATFORMS", []): + with _patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) - assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS - assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT + assert entry.options[CONF_FFMPEG_ARGUMENTS] == DEFAULT_FFMPEG_ARGUMENTS + assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] is None + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] is None - with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FFMPEG_ARGUMENTS: "/H.264", CONF_TIMEOUT: 25}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 - assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 1 async def test_user_form_exception(hass, ezviz_config_flow): From 3bfcca2bb06c2d8b51e53cf240fb8eb3633d375d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:23:56 +0200 Subject: [PATCH 485/750] Add state class to Atome Linky, use class attributes (#52107) --- homeassistant/components/atome/sensor.py | 51 +++++++++--------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f47dd4033b9..bcb7b4f1ece 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -5,7 +5,11 @@ import logging from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -219,37 +223,16 @@ class AtomeSensor(SensorEntity): def __init__(self, data, name, sensor_type): """Initialize the sensor.""" - self._name = name + self._attr_name = name self._data = data - self._state = None - self._attributes = {} self._sensor_type = sensor_type if sensor_type == LIVE_TYPE: - self._unit_of_measurement = POWER_WATT + self._attr_unit_of_measurement = POWER_WATT + self._attr_state_class = STATE_CLASS_MEASUREMENT else: - self._unit_of_measurement = ENERGY_KILO_WATT_HOUR - - @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 extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement + self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR def update(self): """Update device state.""" @@ -257,11 +240,13 @@ class AtomeSensor(SensorEntity): update_function() if self._sensor_type == LIVE_TYPE: - self._state = self._data.live_power - self._attributes["subscribed_power"] = self._data.subscribed_power - self._attributes["is_connected"] = self._data.is_connected + self._attr_state = self._data.live_power + self._attr_extra_state_attributes = { + "subscribed_power": self._data.subscribed_power, + "is_connected": self._data.is_connected, + } else: - self._state = getattr(self._data, f"{self._sensor_type}_usage") - self._attributes["price"] = getattr( - self._data, f"{self._sensor_type}_price" - ) + self._attr_state = getattr(self._data, f"{self._sensor_type}_usage") + self._attr_extra_state_attributes = { + "price": getattr(self._data, f"{self._sensor_type}_price") + } From 39b090d957218d921fd5ed1638304bb11b511d0b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:41:48 +0200 Subject: [PATCH 486/750] Add state class to Neurio energy (#52117) --- homeassistant/components/neurio_energy/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 2bc17fbecb2..d74d6338c8b 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -6,7 +6,11 @@ import neurio import requests.exceptions import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -135,6 +139,7 @@ class NeurioEnergy(SensorEntity): if sensor_type == ACTIVE_TYPE: self._unit_of_measurement = POWER_WATT + self._attr_state_class = STATE_CLASS_MEASUREMENT elif sensor_type == DAILY_TYPE: self._unit_of_measurement = ENERGY_KILO_WATT_HOUR From b9e6a6b3b8ebd4716f88692100b4a7e37220365b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:43:31 +0200 Subject: [PATCH 487/750] Add state class to JuiceNet (#52116) --- homeassistant/components/juicenet/sensor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index d908dc069ef..7564c6e4344 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,5 +1,5 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, @@ -13,13 +13,13 @@ from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice SENSOR_TYPES = { - "status": ["Charging Status", None], - "temperature": ["Temperature", TEMP_CELSIUS], - "voltage": ["Voltage", VOLT], - "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE], - "watts": ["Watts", POWER_WATT], - "charge_time": ["Charge time", TIME_SECONDS], - "energy_added": ["Energy added", ENERGY_WATT_HOUR], + "status": ["Charging Status", None, None], + "temperature": ["Temperature", TEMP_CELSIUS, STATE_CLASS_MEASUREMENT], + "voltage": ["Voltage", VOLT, None], + "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE, STATE_CLASS_MEASUREMENT], + "watts": ["Watts", POWER_WATT, STATE_CLASS_MEASUREMENT], + "charge_time": ["Charge time", TIME_SECONDS, None], + "energy_added": ["Energy added", ENERGY_WATT_HOUR, None], } @@ -44,6 +44,7 @@ class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): super().__init__(device, sensor_type, coordinator) self._name = SENSOR_TYPES[sensor_type][0] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._attr_state_class = SENSOR_TYPES[sensor_type][2] @property def name(self): From 0ddd858b4bd43bab4df6408a99a7bb9c955b09a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:43:57 +0200 Subject: [PATCH 488/750] Add state class to Aurora ABB Solar PV (#52108) --- homeassistant/components/aurora_abb_powerone/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index f4640e7c014..cd4a71d1b31 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -5,7 +5,11 @@ import logging from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, @@ -46,6 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AuroraABBSolarPVMonitorSensor(SensorEntity): """Representation of a Sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, client, name, typename): """Initialize the sensor.""" self._name = f"{name} {typename}" From 927b74b4a2de611a18856ef2fe6d6f90d24dce3b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:44:25 +0200 Subject: [PATCH 489/750] Add state class to The Energy Detective TED5000 (#52109) --- homeassistant/components/ted5000/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 62cdd5066ad..5c439651ed5 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -7,7 +7,11 @@ import requests import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT, VOLT from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -52,6 +56,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class Ted5000Sensor(SensorEntity): """Implementation of a Ted5000 sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, gateway, name, mtu, unit): """Initialize the sensor.""" units = {POWER_WATT: "power", VOLT: "voltage"} From 7f7c0febd80847d23ad41851b88b0ee6cbc5ed02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:44:41 +0200 Subject: [PATCH 490/750] Add state class to DTE Energy Bridge (#52110) --- homeassistant/components/dte_energy_bridge/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 27475990de0..4e095955818 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -4,7 +4,11 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import CONF_NAME, HTTP_OK import homeassistant.helpers.config_validation as cv @@ -41,6 +45,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DteEnergyBridgeSensor(SensorEntity): """Implementation of the DTE Energy Bridge sensors.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, ip_address, name, version): """Initialize the sensor.""" self._version = version From 38daf94562c06379ddf59173a9136827a57daf7d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:44:55 +0200 Subject: [PATCH 491/750] Add state class to Eliqonline (#52111) --- homeassistant/components/eliqonline/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index a4d812850f7..253913b3779 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -6,7 +6,11 @@ import logging import eliqonline import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -54,6 +58,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EliqSensor(SensorEntity): """Implementation of an ELIQ Online sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, api, channel_id, name): """Initialize the sensor.""" self._name = name From 5b663b1fb9ec9b1a20b2d1649f2f7429cc3bf3ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 19:46:00 +0200 Subject: [PATCH 492/750] Add state class to Enphase Envoy (#52113) --- homeassistant/components/enphase_envoy/const.py | 17 ++++++++++------- .../components/enphase_envoy/sensor.py | 4 ++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 89803d32351..7a1de25e242 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,6 +1,7 @@ """The enphase_envoy component.""" +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT DOMAIN = "enphase_envoy" @@ -12,19 +13,21 @@ COORDINATOR = "coordinator" NAME = "name" SENSORS = { - "production": ("Current Energy Production", POWER_WATT), - "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR), + "production": ("Current Energy Production", POWER_WATT, STATE_CLASS_MEASUREMENT), + "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR, None), "seven_days_production": ( "Last Seven Days Energy Production", ENERGY_WATT_HOUR, + None, ), - "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR), - "consumption": ("Current Energy Consumption", POWER_WATT), - "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR), + "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR, None), + "consumption": ("Current Energy Consumption", POWER_WATT, STATE_CLASS_MEASUREMENT), + "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR, None), "seven_days_consumption": ( "Last Seven Days Energy Consumption", ENERGY_WATT_HOUR, + None, ), - "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR), - "inverters": ("Inverter", POWER_WATT), + "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR, None), + "inverters": ("Inverter", POWER_WATT, STATE_CLASS_MEASUREMENT), } diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 050a497f69e..5ccb540efd0 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -74,6 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.unique_id, serial_number, SENSORS[condition][1], + SENSORS[condition][2], coordinator, ) ) @@ -91,6 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.unique_id, None, SENSORS[condition][1], + SENSORS[condition][2], coordinator, ) ) @@ -109,6 +111,7 @@ class Envoy(CoordinatorEntity, SensorEntity): device_serial_number, serial_number, unit, + state_class, coordinator, ): """Initialize Envoy entity.""" @@ -118,6 +121,7 @@ class Envoy(CoordinatorEntity, SensorEntity): self._device_name = device_name self._device_serial_number = device_serial_number self._unit_of_measurement = unit + self._attr_state_class = state_class super().__init__(coordinator) From 1f4fdb50dc41071f15ac784525d58d3986b1d617 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 23 Jun 2021 20:03:17 +0200 Subject: [PATCH 493/750] Share struct validator between sensor and climate (#51935) --- homeassistant/components/modbus/__init__.py | 4 ++- homeassistant/components/modbus/climate.py | 33 --------------------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 4c4a1390887..ed797891502 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -283,7 +283,9 @@ MODBUS_SCHEMA = vol.Schema( vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] ), - vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), + vol.Optional(CONF_CLIMATES): vol.All( + cv.ensure_list, [vol.All(CLIMATE_SCHEMA, sensor_schema_validator)] + ), vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHT_SCHEMA]), vol.Optional(CONF_SENSORS): vol.All( diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index a178f6f0f84..5c99ac86d6c 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -40,8 +40,6 @@ from .const import ( CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, - DATA_TYPE_CUSTOM, - DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -63,37 +61,6 @@ async def async_setup_platform( entities = [] for entity in discovery_info[CONF_CLIMATES]: hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] - count = entity[CONF_COUNT] - data_type = entity[CONF_DATA_TYPE] - name = entity[CONF_NAME] - structure = entity[CONF_STRUCTURE] - - if data_type != DATA_TYPE_CUSTOM: - try: - structure = f">{DEFAULT_STRUCT_FORMAT[data_type][count]}" - except KeyError: - _LOGGER.error( - "Climate %s: Unable to find a data type matching count value %s, try a custom type", - name, - count, - ) - continue - - try: - size = struct.calcsize(structure) - except struct.error as err: - _LOGGER.error("Error in sensor %s structure: %s", name, err) - continue - - if count * 2 != size: - _LOGGER.error( - "Structure size (%d bytes) mismatch registers count (%d words)", - size, - count, - ) - continue - - entity[CONF_STRUCTURE] = structure entities.append(ModbusThermostat(hub, entity)) async_add_entities(entities) From 6352d8fb0ebf7af9fb5b28db18fe8838adeb1bd4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 23 Jun 2021 21:40:34 +0200 Subject: [PATCH 494/750] Use more attr instead of properties in deCONZ integration (#52098) --- .../components/deconz/binary_sensor.py | 21 ++--- homeassistant/components/deconz/climate.py | 11 +-- homeassistant/components/deconz/const.py | 7 +- homeassistant/components/deconz/cover.py | 43 ++++----- .../components/deconz/deconz_device.py | 18 ++-- homeassistant/components/deconz/fan.py | 4 +- homeassistant/components/deconz/light.py | 4 +- homeassistant/components/deconz/scene.py | 7 +- homeassistant/components/deconz/sensor.py | 89 ++++++------------- 9 files changed, 80 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index b19301aa113..392d2e03885 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -81,6 +81,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN + def __init__(self, device, gateway): + """Initialize deCONZ binary sensor.""" + super().__init__(device, gateway) + self._attr_device_class = DEVICE_CLASS.get(type(self._device)) + @callback def async_update_callback(self, force_update=False): """Update the sensor's state.""" @@ -93,11 +98,6 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Return true if sensor is on.""" return self._device.state - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS.get(type(self._device)) - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -129,6 +129,12 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): _attr_device_class = DEVICE_CLASS_PROBLEM + def __init__(self, device, gateway): + """Initialize deCONZ binary sensor.""" + super().__init__(device, gateway) + + self._attr_name = f"{self._device.name} Tampered" + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" @@ -145,8 +151,3 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): def is_on(self) -> bool: """Return the state of the sensor.""" return self._device.tampered - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device.name} Tampered" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index db68843080a..4f8345b5e92 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -128,18 +128,13 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): value: key for key, value in self._hvac_mode_to_deconz.items() } - self._features = SUPPORT_TARGET_TEMPERATURE + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE if "fanmode" in device.raw["config"]: - self._features |= SUPPORT_FAN_MODE + self._attr_supported_features |= SUPPORT_FAN_MODE if "preset" in device.raw["config"]: - self._features |= SUPPORT_PRESET_MODE - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._features + self._attr_supported_features |= SUPPORT_PRESET_MODE # Fan control diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 799fc221e2c..d2d7025771e 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -56,8 +56,11 @@ ATTR_ON = "on" ATTR_VALVE = "valve" # Covers -DAMPERS = ["Level controllable output"] -WINDOW_COVERS = ["Window covering device", "Window covering controller"] +LEVEL_CONTROLLABLE_OUTPUT = "Level controllable output" +DAMPERS = [LEVEL_CONTROLLABLE_OUTPUT] +WINDOW_COVERING_CONTROLLER = "Window covering controller" +WINDOW_COVERING_DEVICE = "Window covering device" +WINDOW_COVERS = [WINDOW_COVERING_CONTROLLER, WINDOW_COVERING_DEVICE] COVER_TYPES = DAMPERS + WINDOW_COVERS # Fans diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 68fb9527e87..21618127905 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -18,10 +18,22 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS +from .const import ( + COVER_TYPES, + LEVEL_CONTROLLABLE_OUTPUT, + NEW_LIGHT, + WINDOW_COVERING_CONTROLLER, + WINDOW_COVERING_DEVICE, +) from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +DEVICE_CLASS = { + LEVEL_CONTROLLABLE_OUTPUT: DEVICE_CLASS_DAMPER, + WINDOW_COVERING_CONTROLLER: DEVICE_CLASS_SHADE, + WINDOW_COVERING_DEVICE: DEVICE_CLASS_SHADE, +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up covers for deCONZ component.""" @@ -61,29 +73,18 @@ class DeconzCover(DeconzDevice, CoverEntity): """Set up cover device.""" super().__init__(device, gateway) - self._features = SUPPORT_OPEN - self._features |= SUPPORT_CLOSE - self._features |= SUPPORT_STOP - self._features |= SUPPORT_SET_POSITION + self._attr_supported_features = SUPPORT_OPEN + self._attr_supported_features |= SUPPORT_CLOSE + self._attr_supported_features |= SUPPORT_STOP + self._attr_supported_features |= SUPPORT_SET_POSITION if self._device.tilt is not None: - self._features |= SUPPORT_OPEN_TILT - self._features |= SUPPORT_CLOSE_TILT - self._features |= SUPPORT_STOP_TILT - self._features |= SUPPORT_SET_TILT_POSITION + self._attr_supported_features |= SUPPORT_OPEN_TILT + self._attr_supported_features |= SUPPORT_CLOSE_TILT + self._attr_supported_features |= SUPPORT_STOP_TILT + self._attr_supported_features |= SUPPORT_SET_TILT_POSITION - @property - def supported_features(self): - """Flag supported features.""" - return self._features - - @property - def device_class(self): - """Return the class of the cover.""" - if self._device.type in DAMPERS: - return DEVICE_CLASS_DAMPER - if self._device.type in WINDOW_COVERS: - return DEVICE_CLASS_SHADE + self._attr_device_class = DEVICE_CLASS.get(self._device.type) @property def current_cover_position(self): diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 37a41845a00..63f624ba643 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -34,8 +34,6 @@ class DeconzBase: if self.serial is None: return None - bridgeid = self.gateway.api.config.bridgeid - return { "connections": {(CONNECTION_ZIGBEE, self.serial)}, "identifiers": {(DECONZ_DOMAIN, self.serial)}, @@ -43,13 +41,15 @@ class DeconzBase: "model": self._device.modelid, "name": self._device.name, "sw_version": self._device.swversion, - "via_device": (DECONZ_DOMAIN, bridgeid), + "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridgeid), } class DeconzDevice(DeconzBase, Entity): """Representation of a deCONZ device.""" + _attr_should_poll = False + TYPE = "" def __init__(self, device, gateway): @@ -57,6 +57,8 @@ class DeconzDevice(DeconzBase, Entity): super().__init__(device, gateway) self.gateway.entities[self.TYPE].add(self.unique_id) + self._attr_name = self._device.name + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry. @@ -93,13 +95,3 @@ class DeconzDevice(DeconzBase, Entity): def available(self): """Return True if device is available.""" return self.gateway.available and self._device.reachable - - @property - def name(self): - """Return the name of the device.""" - return self._device.name - - @property - def should_poll(self): - """No polling needed.""" - return False diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index dfb6802fd75..cb64bff6d16 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -67,7 +67,7 @@ class DeconzFan(DeconzDevice, FanEntity): if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed - self._features = SUPPORT_SET_SPEED + self._attr_supported_features = SUPPORT_SET_SPEED @property def is_on(self) -> bool: @@ -128,7 +128,7 @@ class DeconzFan(DeconzDevice, FanEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return self._features + return self._attr_supported_features @callback def async_update_callback(self, force_update=False) -> None: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ac18d2de248..90d5e82af71 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -273,14 +273,12 @@ class DeconzGroup(DeconzBaseLight): @property def device_info(self): """Return a device description for device registry.""" - bridgeid = self.gateway.api.config.bridgeid - return { "identifiers": {(DECONZ_DOMAIN, self.unique_id)}, "manufacturer": "Dresden Elektronik", "model": "deCONZ group", "name": self._device.name, - "via_device": (DECONZ_DOMAIN, bridgeid), + "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridgeid), } @property diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index ecd363f121a..f4a4d328d22 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -38,6 +38,8 @@ class DeconzScene(Scene): self._scene = scene self.gateway = gateway + self._attr_name = scene.full_name + async def async_added_to_hass(self): """Subscribe to sensors events.""" self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id @@ -50,8 +52,3 @@ class DeconzScene(Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" await self._scene.async_set_state({}) - - @property - def name(self): - """Return the name of the scene.""" - return self._scene.full_name diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index bbc49f786ea..e0f12303946 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -149,6 +149,15 @@ class DeconzSensor(DeconzDevice, SensorEntity): TYPE = DOMAIN + def __init__(self, device, gateway): + """Initialize deCONZ binary sensor.""" + super().__init__(device, gateway) + + self._attr_device_class = DEVICE_CLASS.get(type(self._device)) + self._attr_icon = ICON.get(type(self._device)) + self._attr_state_class = STATE_CLASS.get(type(self._device)) + self._attr_unit_of_measurement = UNIT_OF_MEASUREMENT.get(type(self._device)) + @callback def async_update_callback(self, force_update=False): """Update the sensor's state.""" @@ -161,26 +170,6 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Return the state of the sensor.""" return self._device.state - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS.get(type(self._device)) - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON.get(type(self._device)) - - @property - def state_class(self): - """Return the state class of the sensor.""" - return STATE_CLASS.get(type(self._device)) - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return UNIT_OF_MEASUREMENT.get(type(self._device)) - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -219,8 +208,18 @@ class DeconzTemperature(DeconzDevice, SensorEntity): Extra temperature sensor on certain Xiaomi devices. """ + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = TEMP_CELSIUS + TYPE = DOMAIN + def __init__(self, device, gateway): + """Initialize deCONZ temperature sensor.""" + super().__init__(device, gateway) + + self._attr_name = f"{self._device.name} Temperature" + @property def unique_id(self): """Return a unique identifier for this device.""" @@ -238,32 +237,22 @@ class DeconzTemperature(DeconzDevice, SensorEntity): """Return the state of the sensor.""" return self._device.secondary_temperature - @property - def name(self): - """Return the name of the temperature sensor.""" - return f"{self._device.name} Temperature" - - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def state_class(self): - """Return the state class of the sensor.""" - return STATE_CLASS_MEASUREMENT - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return TEMP_CELSIUS - class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_unit_of_measurement = PERCENTAGE + TYPE = DOMAIN + def __init__(self, device, gateway): + """Initialize deCONZ battery level sensor.""" + super().__init__(device, gateway) + + self._attr_name = f"{self._device.name} Battery Level" + @callback def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" @@ -292,26 +281,6 @@ class DeconzBattery(DeconzDevice, SensorEntity): """Return the state of the battery.""" return self._device.battery - @property - def name(self): - """Return the name of the battery.""" - return f"{self._device.name} Battery Level" - - @property - def device_class(self): - """Return the class of the sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def state_class(self): - """Return the state class of the sensor.""" - return STATE_CLASS_MEASUREMENT - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes of the battery.""" From cc00617cd52a9c453676b9c7978115295f181fd8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Jun 2021 14:37:04 -0700 Subject: [PATCH 495/750] Allow defining state class for template sensors (#52130) --- homeassistant/components/template/const.py | 1 + homeassistant/components/template/light.py | 2 +- homeassistant/components/template/sensor.py | 58 ++++++++----------- .../components/template/template_entity.py | 50 +++++----------- homeassistant/components/template/vacuum.py | 2 +- tests/components/template/test_sensor.py | 30 ++++++++++ 6 files changed, 70 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 661953bcfa5..31896e930e4 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -25,3 +25,4 @@ CONF_AVAILABILITY = "availability" CONF_ATTRIBUTES = "attributes" CONF_PICTURE = "picture" CONF_OBJECT_ID = "object_id" +CONF_STATE_CLASS = "state_class" diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index f546c5dc4da..b8ebe03ceba 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -595,7 +595,7 @@ class LightTemplate(TemplateEntity, LightEntity): # This behavior is legacy self._state = False if not self._availability_template: - self._available = True + self._attr_available = True return if isinstance(result, bool): diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 56e0e11edb0..a887890510a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + STATE_CLASSES_SCHEMA, SensorEntity, ) from homeassistant.const import ( @@ -37,6 +38,7 @@ from .const import ( CONF_AVAILABILITY_TEMPLATE, CONF_OBJECT_ID, CONF_PICTURE, + CONF_STATE_CLASS, CONF_TRIGGER, ) from .template_entity import TemplateEntity @@ -64,6 +66,7 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, } ) @@ -159,6 +162,7 @@ def _async_create_template_tracking_entities( device_class = entity_conf.get(CONF_DEVICE_CLASS) attribute_templates = entity_conf.get(CONF_ATTRIBUTES, {}) unique_id = entity_conf.get(CONF_UNIQUE_ID) + state_class = entity_conf.get(CONF_STATE_CLASS) if unique_id and unique_id_prefix: unique_id = f"{unique_id_prefix}-{unique_id}" @@ -176,6 +180,7 @@ def _async_create_template_tracking_entities( device_class, attribute_templates, unique_id, + state_class, ) ) @@ -224,6 +229,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): device_class: str | None, attribute_templates: dict[str, template.Template], unique_id: str | None, + state_class: str | None, ) -> None: """Initialize the sensor.""" super().__init__( @@ -237,61 +243,38 @@ class SensorTemplate(TemplateEntity, SensorEntity): ENTITY_ID_FORMAT, object_id, hass=hass ) - self._name: str | None = None self._friendly_name_template = friendly_name_template # Try to render the name as it can influence the entity ID if friendly_name_template: friendly_name_template.hass = hass try: - self._name = friendly_name_template.async_render(parse_result=False) + self._attr_name = friendly_name_template.async_render( + parse_result=False + ) except template.TemplateError: pass - self._unit_of_measurement = unit_of_measurement + self._attr_unit_of_measurement = unit_of_measurement self._template = state_template - self._state = None - self._device_class = device_class - - self._unique_id = unique_id + self._attr_device_class = device_class + self._attr_state_class = state_class + self._attr_unique_id = unique_id async def async_added_to_hass(self): """Register callbacks.""" - self.add_template_attribute("_state", self._template, None, self._update_state) + self.add_template_attribute( + "_attr_state", self._template, None, self._update_state + ) if self._friendly_name_template and not self._friendly_name_template.is_static: - self.add_template_attribute("_name", self._friendly_name_template) + self.add_template_attribute("_attr_name", self._friendly_name_template) await super().async_added_to_hass() @callback def _update_state(self, result): super()._update_state(result) - self._state = None if isinstance(result, TemplateError) else result - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self): - """Return the unique id of this sensor.""" - return self._unique_id - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._device_class - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return self._unit_of_measurement + self._attr_state = None if isinstance(result, TemplateError) else result class TriggerSensorEntity(TriggerEntity, SensorEntity): @@ -304,3 +287,8 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity): def state(self) -> str | None: """Return state of the sensor.""" return self._rendered.get(CONF_STATE) + + @property + def state_class(self) -> str | None: + """Sensor state class.""" + return self._config.get(CONF_STATE_CLASS) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 522eb7d89ba..7bf6d6109be 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -112,6 +112,8 @@ class _TemplateAttribute: class TemplateEntity(Entity): """Entity that uses templates to calculate attributes.""" + _attr_should_poll = False + def __init__( self, *, @@ -124,54 +126,27 @@ class TemplateEntity(Entity): self._template_attrs = {} self._async_update = None self._attribute_templates = attribute_templates - self._attributes = {} + self._attr_extra_state_attributes = {} self._availability_template = availability_template - self._available = True + self._attr_available = True self._icon_template = icon_template self._entity_picture_template = entity_picture_template - self._icon = None - self._entity_picture = None self._self_ref_update_count = 0 - @property - def should_poll(self): - """No polling needed.""" - return False - @callback def _update_available(self, result): if isinstance(result, TemplateError): - self._available = True + self._attr_available = True return - self._available = result_as_boolean(result) + self._attr_available = result_as_boolean(result) @callback def _update_state(self, result): if self._availability_template: return - self._available = not isinstance(result, TemplateError) - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._available - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - @property - def entity_picture(self): - """Return the entity_picture to use in the frontend, if any.""" - return self._entity_picture - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes + self._attr_available = not isinstance(result, TemplateError) @callback def _add_attribute_template(self, attribute_key, attribute_template): @@ -179,7 +154,7 @@ class TemplateEntity(Entity): def _update_attribute(result): attr_result = None if isinstance(result, TemplateError) else result - self._attributes[attribute_key] = attr_result + self._attr_extra_state_attributes[attribute_key] = attr_result self.add_template_attribute( attribute_key, attribute_template, None, _update_attribute @@ -271,18 +246,21 @@ class TemplateEntity(Entity): """Run when entity about to be added to hass.""" if self._availability_template is not None: self.add_template_attribute( - "_available", self._availability_template, None, self._update_available + "_attr_available", + self._availability_template, + None, + self._update_available, ) if self._attribute_templates is not None: for key, value in self._attribute_templates.items(): self._add_attribute_template(key, value) if self._icon_template is not None: self.add_template_attribute( - "_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) + "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) ) if self._entity_picture_template is not None: self.add_template_attribute( - "_entity_picture", self._entity_picture_template + "_attr_entity_picture", self._entity_picture_template ) if self.hass.state == CoreState.running: await self._async_template_startup() diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index ed7919d174e..78c51d2009c 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -362,7 +362,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): # This is legacy behavior self._state = STATE_UNKNOWN if not self._availability_template: - self._available = True + self._attr_available = True return # Validate state diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 4047a822432..df5c43aa58b 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1044,6 +1044,7 @@ async def test_trigger_entity(hass): "attributes": { "plus_one": "{{ trigger.event.data.beer + 1 }}" }, + "state_class": "measurement", } ], }, @@ -1100,6 +1101,7 @@ async def test_trigger_entity(hass): assert state.attributes.get("entity_picture") == "/local/dogs.png" assert state.attributes.get("plus_one") == 3 assert state.attributes.get("unit_of_measurement") == "%" + assert state.attributes.get("state_class") == "measurement" assert state.context is context @@ -1167,3 +1169,31 @@ async def test_trigger_not_allowed_platform_config(hass, caplog): "You can only add triggers to template entities if they are defined under `template:`." in caplog.text ) + + +async def test_config_top_level(hass): + """Test unique_id option only creates one sensor per id.""" + await async_setup_component( + hass, + "template", + { + "template": { + "sensor": { + "name": "top-level", + "device_class": "battery", + "state_class": "measurement", + "state": "5", + "unit_of_measurement": "%", + }, + }, + }, + ) + + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + state = hass.states.get("sensor.top_level") + assert state is not None + assert state.state == "5" + assert state.attributes["device_class"] == "battery" + assert state.attributes["state_class"] == "measurement" From a67ca08124d96764a3031d70907323fb5eb03d73 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Jun 2021 23:43:24 +0200 Subject: [PATCH 496/750] Change dynamic segment handling of WLED (#52018) --- homeassistant/components/wled/__init__.py | 3 +- homeassistant/components/wled/coordinator.py | 27 ++++++- homeassistant/components/wled/light.py | 85 ++++++-------------- tests/components/wled/test_light.py | 75 ++++++++++------- 4 files changed, 93 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 32782338fe5..77b97472747 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -5,7 +5,6 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -16,7 +15,7 @@ PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WLED from a config entry.""" - coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + coordinator = WLEDDataUpdateCoordinator(hass, entry=entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 16d56705879..b730ac1543a 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -6,25 +6,37 @@ from typing import Callable from wled import WLED, Device as WLEDDevice, WLEDConnectionClosed, WLEDError -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER, SCAN_INTERVAL +from .const import ( + CONF_KEEP_MASTER_LIGHT, + DEFAULT_KEEP_MASTER_LIGHT, + DOMAIN, + LOGGER, + SCAN_INTERVAL, +) class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): """Class to manage fetching WLED data from single endpoint.""" + keep_master_light: bool + def __init__( self, hass: HomeAssistant, *, - host: str, + entry: ConfigEntry, ) -> None: """Initialize global WLED data updater.""" - self.wled = WLED(host, session=async_get_clientsession(hass)) + self.keep_master_light = entry.options.get( + CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT + ) + self.wled = WLED(entry.data[CONF_HOST], session=async_get_clientsession(hass)) self.unsub: Callable | None = None super().__init__( @@ -34,6 +46,13 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): update_interval=SCAN_INTERVAL, ) + @property + def has_master_light(self) -> bool: + """Return if the coordinated device has an master light.""" + return self.keep_master_light or ( + self.data is not None and len(self.data.state.segments) > 1 + ) + def update_listeners(self) -> None: """Call update on all listeners.""" for update_callback in self._listeners: diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 533cb595638..0cb45caab87 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -24,9 +24,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) from .const import ( ATTR_COLOR_PRIMARY, @@ -38,8 +35,6 @@ from .const import ( ATTR_REVERSE, ATTR_SEGMENT_ID, ATTR_SPEED, - CONF_KEEP_MASTER_LIGHT, - DEFAULT_KEEP_MASTER_LIGHT, DOMAIN, SERVICE_EFFECT, SERVICE_PRESET, @@ -87,17 +82,13 @@ async def async_setup_entry( "async_preset", ) - keep_master_light = entry.options.get( - CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT - ) - if keep_master_light: + if coordinator.keep_master_light: async_add_entities([WLEDMasterLight(coordinator=coordinator)]) update_segments = partial( async_update_segments, entry, coordinator, - keep_master_light, {}, async_add_entities, ) @@ -130,6 +121,11 @@ class WLEDMasterLight(WLEDEntity, LightEntity): """Return the state of the light.""" return bool(self.coordinator.data.state.on) + @property + def available(self) -> bool: + """Return if this master light is available or not.""" + return self.coordinator.has_master_light and super().available + @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" @@ -182,18 +178,17 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): self, coordinator: WLEDDataUpdateCoordinator, segment: int, - keep_master_light: bool, ) -> None: """Initialize WLED segment light.""" super().__init__(coordinator=coordinator) - self._keep_master_light = keep_master_light self._rgbw = coordinator.data.info.leds.rgbw self._wv = coordinator.data.info.leds.wv self._segment = segment - # If this is the one and only segment, use a simpler name + # Segment 0 uses a simpler name, which is more natural for when using + # a single segment / using WLED with one big LED strip. self._attr_name = f"{coordinator.data.info.name} Segment {segment}" - if len(coordinator.data.state.segments) == 1: + if segment == 0: self._attr_name = coordinator.data.info.name self._attr_unique_id = ( @@ -264,7 +259,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # If this is the one and only segment, calculate brightness based # on the master and segment brightness - if not self._keep_master_light and len(state.segments) == 1: + if not self.coordinator.has_master_light: return int( (state.segments[self._segment].brightness * state.brightness) / 255 ) @@ -281,8 +276,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): """Return the state of the light.""" state = self.coordinator.data.state - # If there is a single segment, take master into account - if len(state.segments) == 1 and not state.on: + # If there is no master, we take the master state into account + # on the segment level. + if not self.coordinator.has_master_light and not state.on: return False return bool(state.segments[self._segment].on) @@ -295,11 +291,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # WLED uses 100ms per unit, so 10 = 1 second. transition = round(kwargs[ATTR_TRANSITION] * 10) - # If there is a single segment, control via the master - if ( - not self._keep_master_light - and len(self.coordinator.data.state.segments) == 1 - ): + # If there is no master control, and only 1 segment, handle the + if not self.coordinator.has_master_light: await self.coordinator.wled.master(on=False, transition=transition) return @@ -331,12 +324,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if ATTR_EFFECT in kwargs: data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] - # When only 1 segment is present, switch along the master, and use - # the master for power/brightness control. - if ( - not self._keep_master_light - and len(self.coordinator.data.state.segments) == 1 - ): + # If there is no master control, and only 1 segment, handle the master + if not self.coordinator.has_master_light: master_data = {ATTR_ON: True} if ATTR_BRIGHTNESS in data: master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS] @@ -384,56 +373,28 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): def async_update_segments( entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, - keep_master_light: bool, current: dict[int, WLEDSegmentLight | WLEDMasterLight], async_add_entities, ) -> None: """Update segments.""" segment_ids = {light.segment_id for light in coordinator.data.state.segments} current_ids = set(current) + new_entities = [] # Discard master (if present) current_ids.discard(-1) - new_entities = [] - # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDSegmentLight( - coordinator, segment_id, keep_master_light - ) + current[segment_id] = WLEDSegmentLight(coordinator, segment_id) new_entities.append(current[segment_id]) - # More than 1 segment now? Add master controls - if not keep_master_light and (len(current_ids) < 2 and len(segment_ids) > 1): + # More than 1 segment now? No master? Add master controls + if not coordinator.keep_master_light and ( + len(current_ids) < 2 and len(segment_ids) > 1 + ): current[-1] = WLEDMasterLight(coordinator) new_entities.append(current[-1]) if new_entities: async_add_entities(new_entities) - - # Process deleted segments, remove them from Home Assistant - for segment_id in current_ids - segment_ids: - coordinator.hass.async_create_task( - async_remove_entity(segment_id, coordinator, current) - ) - - # Remove master if there is only 1 segment left - if not keep_master_light and len(current_ids) > 1 and len(segment_ids) < 2: - coordinator.hass.async_create_task( - async_remove_entity(-1, coordinator, current) - ) - - -async def async_remove_entity( - index: int, - coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight | WLEDMasterLight], -) -> None: - """Remove WLED segment light from Home Assistant.""" - entity = current[index] - await entity.async_remove(force_remove=True) - registry = await async_get_entity_registry(coordinator.hass) - if entity.entity_id in registry.entities: - registry.async_remove(entity.entity_id) - del current[index] diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index a4e3f712547..d61b675e2f2 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -50,7 +50,7 @@ async def test_rgb_light_state( entity_registry = er.async_get(hass) # First segment of the strip - state = hass.states.get("light.wled_rgb_light_segment_0") + state = hass.states.get("light.wled_rgb_light") assert state assert state.attributes.get(ATTR_BRIGHTNESS) == 127 assert state.attributes.get(ATTR_EFFECT) == "Solid" @@ -64,7 +64,7 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_SPEED) == 32 assert state.state == STATE_ON - entry = entity_registry.async_get("light.wled_rgb_light_segment_0") + entry = entity_registry.async_get("light.wled_rgb_light") assert entry assert entry.unique_id == "aabbccddeeff_0" @@ -107,7 +107,7 @@ async def test_segment_change_state( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, blocking=True, ) await hass.async_block_till_done() @@ -124,7 +124,7 @@ async def test_segment_change_state( { ATTR_BRIGHTNESS: 42, ATTR_EFFECT: "Chase", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_RGB_COLOR: [255, 0, 0], ATTR_TRANSITION: 5, }, @@ -211,36 +211,53 @@ async def test_master_change_state( ) +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) async def test_dynamically_handle_segments( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" - assert hass.states.get("light.wled_rgb_light_master") - assert hass.states.get("light.wled_rgb_light_segment_0") - assert hass.states.get("light.wled_rgb_light_segment_1") + master = hass.states.get("light.wled_rgb_light_master") + segment0 = hass.states.get("light.wled_rgb_light") + segment1 = hass.states.get("light.wled_rgb_light_segment_1") + assert segment0 + assert segment0.state == STATE_ON + assert not master + assert not segment1 return_value = mock_wled.update.return_value mock_wled.update.return_value = WLEDDevice( - json.loads(load_fixture("wled/rgb_single_segment.json")) + json.loads(load_fixture("wled/rgb.json")) ) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - assert hass.states.get("light.wled_rgb_light_segment_0") - assert not hass.states.get("light.wled_rgb_light_segment_1") - assert not hass.states.get("light.wled_rgb_light_master") + master = hass.states.get("light.wled_rgb_light_master") + segment0 = hass.states.get("light.wled_rgb_light") + segment1 = hass.states.get("light.wled_rgb_light_segment_1") + assert master + assert master.state == STATE_ON + assert segment0 + assert segment0.state == STATE_ON + assert segment1 + assert segment1.state == STATE_ON # Test adding if segment shows up again, including the master entity mock_wled.update.return_value = return_value async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - assert hass.states.get("light.wled_rgb_light_master") - assert hass.states.get("light.wled_rgb_light_segment_0") - assert hass.states.get("light.wled_rgb_light_segment_1") + master = hass.states.get("light.wled_rgb_light_master") + segment0 = hass.states.get("light.wled_rgb_light") + segment1 = hass.states.get("light.wled_rgb_light_segment_1") + assert master + assert master.state == STATE_UNAVAILABLE + assert segment0 + assert segment0.state == STATE_ON + assert segment1 + assert segment1.state == STATE_UNAVAILABLE @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) @@ -320,12 +337,12 @@ async def test_light_error( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") + state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_ON assert "Invalid response from API" in caplog.text @@ -345,12 +362,12 @@ async def test_light_connection_error( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") + state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text @@ -395,7 +412,7 @@ async def test_effect_service( SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_INTENSITY: 200, ATTR_PALETTE: "Tiamat", ATTR_REVERSE: True, @@ -417,7 +434,7 @@ async def test_effect_service( await hass.services.async_call( DOMAIN, SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, blocking=True, ) await hass.async_block_till_done() @@ -435,7 +452,7 @@ async def test_effect_service( DOMAIN, SERVICE_EFFECT, { - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_INTENSITY: 200, ATTR_REVERSE: True, ATTR_SPEED: 100, @@ -458,7 +475,7 @@ async def test_effect_service( SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_PALETTE: "Tiamat", ATTR_REVERSE: True, ATTR_SPEED: 100, @@ -481,7 +498,7 @@ async def test_effect_service( SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_INTENSITY: 200, ATTR_SPEED: 100, }, @@ -503,7 +520,7 @@ async def test_effect_service( SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_INTENSITY: 200, ATTR_REVERSE: True, }, @@ -533,12 +550,12 @@ async def test_effect_service_error( await hass.services.async_call( DOMAIN, SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") + state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_ON assert "Invalid response from API" in caplog.text @@ -556,7 +573,7 @@ async def test_preset_service( DOMAIN, SERVICE_PRESET, { - ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_PRESET: 1, }, blocking=True, @@ -591,12 +608,12 @@ async def test_preset_service_error( await hass.services.async_call( DOMAIN, SERVICE_PRESET, - {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_PRESET: 1}, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_PRESET: 1}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_segment_0") + state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_ON assert "Invalid response from API" in caplog.text From 0714ee68eb86b516c2c889a5ef12504eef9771cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jun 2021 09:52:17 +0200 Subject: [PATCH 497/750] Bump docker/login-action from 1.9.0 to 1.10.0 (#52140) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index e62bb19af49..0c30f6887b2 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -102,13 +102,13 @@ jobs: version="$(python setup.py -V)" - name: Login to DockerHub - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -154,13 +154,13 @@ jobs: uses: actions/checkout@v2.3.4 - name: Login to DockerHub - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -217,13 +217,13 @@ jobs: uses: actions/checkout@v2.3.4 - name: Login to DockerHub - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.9.0 + uses: docker/login-action@v1.10.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From aa56a21b4504e572b6f410afbe453a3568af995c Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 24 Jun 2021 10:16:08 +0200 Subject: [PATCH 498/750] Add config flow step user to dsmr (#50318) Co-authored-by: Franck Nijhof --- homeassistant/components/dsmr/config_flow.py | 148 +++++++++- homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dsmr/sensor.py | 4 +- homeassistant/components/dsmr/strings.json | 40 ++- .../components/dsmr/translations/en.json | 38 ++- homeassistant/generated/config_flows.py | 1 + tests/components/dsmr/test_config_flow.py | 266 +++++++++++++++++- 7 files changed, 490 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index b349afb28c7..2312cad215b 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -4,16 +4,18 @@ from __future__ import annotations import asyncio from functools import partial import logging +import os from typing import Any from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader import serial +import serial.tools.list_ports import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import callback from .const import ( @@ -27,6 +29,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +CONF_MANUAL_PATH = "Enter Manually" + class DSMRConnection: """Test the connection to DSMR and receive telegram to read serial ids.""" @@ -124,6 +128,10 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Initialize flow instance.""" + self._dsmr_version = None + @staticmethod @callback def async_get_options_flow(config_entry): @@ -160,6 +168,132 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return None + async def async_step_user(self, user_input=None): + """Step when user initializes a integration.""" + errors = {} + if user_input is not None: + user_selection = user_input[CONF_TYPE] + if user_selection == "Serial": + return await self.async_step_setup_serial() + + return await self.async_step_setup_network() + + list_of_types = ["Serial", "Network"] + + schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_setup_network(self, user_input=None): + """Step when setting up network configuration.""" + errors = {} + + if user_input is not None: + data = await self.async_validate_dsmr(user_input, errors) + + if not errors: + return self.async_create_entry( + title=f"{data[CONF_HOST]}:{data[CONF_PORT]}", data=data + ) + + schema = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT): int, + vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + } + ) + return self.async_show_form( + step_id="setup_network", + data_schema=schema, + errors=errors, + ) + + async def async_step_setup_serial(self, user_input=None): + """Step when setting up serial configuration.""" + errors = {} + + if user_input is not None: + user_selection = user_input[CONF_PORT] + if user_selection == CONF_MANUAL_PATH: + self._dsmr_version = user_input[CONF_DSMR_VERSION] + return await self.async_step_setup_serial_manual_path() + + dev_path = await self.hass.async_add_executor_job( + get_serial_by_id, user_selection + ) + + validate_data = { + CONF_PORT: dev_path, + CONF_DSMR_VERSION: user_input[CONF_DSMR_VERSION], + } + + data = await self.async_validate_dsmr(validate_data, errors) + + if not errors: + return self.async_create_entry(title=data[CONF_PORT], data=data) + + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + list_of_ports = {} + for port in ports: + list_of_ports[ + port.device + ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( + f" - {port.manufacturer}" if port.manufacturer else "" + ) + list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH + + schema = vol.Schema( + { + vol.Required(CONF_PORT): vol.In(list_of_ports), + vol.Required(CONF_DSMR_VERSION): vol.In(["2.2", "4", "5", "5B", "5L"]), + } + ) + return self.async_show_form( + step_id="setup_serial", + data_schema=schema, + errors=errors, + ) + + async def async_step_setup_serial_manual_path(self, user_input=None): + """Select path manually.""" + errors = {} + + if user_input is not None: + validate_data = { + CONF_PORT: user_input[CONF_PORT], + CONF_DSMR_VERSION: self._dsmr_version, + } + + data = await self.async_validate_dsmr(validate_data, errors) + + if not errors: + return self.async_create_entry(title=data[CONF_PORT], data=data) + + schema = vol.Schema({vol.Required(CONF_PORT): str}) + return self.async_show_form( + step_id="setup_serial_manual_path", + data_schema=schema, + errors=errors, + ) + + async def async_validate_dsmr(self, input_data, errors): + """Validate dsmr connection and create data.""" + data = input_data + + try: + info = await _validate_dsmr_connection(self.hass, data) + + data = {**data, **info} + + await self.async_set_unique_id(info[CONF_SERIAL_ID]) + self._abort_if_unique_id_configured() + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotCommunicate: + errors["base"] = "cannot_communicate" + + return data + async def async_step_import(self, import_config=None): """Handle the initial step.""" host = import_config.get(CONF_HOST) @@ -216,6 +350,18 @@ class DSMROptionFlowHandler(config_entries.OptionsFlow): ) +def get_serial_by_id(dev_path: str) -> str: + """Return a /dev/serial/by-id match for given device if available.""" + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return dev_path + + for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): + if os.path.realpath(path) == dev_path: + return path + return dev_path + + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index a5c9b8e62bc..38df6f733cd 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": ["dsmr_parser==0.29"], "codeowners": ["@Robbie1221"], - "config_flow": false, + "config_flow": true, "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 237f3b2f929..00cf5d21e86 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -344,7 +344,9 @@ class DSMREntity(SensorEntity): return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) with suppress(TypeError): - value = round(float(value), self._config[CONF_PRECISION]) + value = round( + float(value), self._config.get(CONF_PRECISION, DEFAULT_PRECISION) + ) if value is not None: return value diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json index 57d38f78feb..cc9cd2ae86a 100644 --- a/homeassistant/components/dsmr/strings.json +++ b/homeassistant/components/dsmr/strings.json @@ -1,9 +1,43 @@ { "config": { - "step": {}, - "error": {}, + "step": { + "user": { + "data": { + "type": "Connection type" + }, + "title": "Select connection type" + }, + "setup_network": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "dsmr_version": "Select DSMR version" + }, + "title": "Select connection address" + }, + "setup_serial": { + "data": { + "port": "Select device", + "dsmr_version": "Select DSMR version" + }, + "title": "Device" + }, + "setup_serial_manual_path": { + "data": { + "port": "[%key:common::config_flow::data::usb_path%]" + }, + "title": "Path" + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_communicate": "Failed to communicate" + }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_communicate": "Failed to communicate" } }, "options": { diff --git a/homeassistant/components/dsmr/translations/en.json b/homeassistant/components/dsmr/translations/en.json index 159ede41b4e..6f873729bc8 100644 --- a/homeassistant/components/dsmr/translations/en.json +++ b/homeassistant/components/dsmr/translations/en.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_communicate": "Failed to communicate", + "cannot_connect": "Failed to connect" + }, + "error": { + "already_configured": "Device is already configured", + "cannot_communicate": "Failed to communicate", + "cannot_connect": "Failed to connect" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "Select DSMR version", + "host": "Host", + "port": "Port" + }, + "title": "Select connection address" + }, + "setup_serial": { + "data": { + "dsmr_version": "Select DSMR version", + "port": "Select device" + }, + "title": "Device" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB Device Path" + }, + "title": "Path" + }, + "user": { + "data": { + "type": "Connection type" + }, + "title": "Select connection type" + } } }, "options": { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6504de85199..7af0bfd129e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -56,6 +56,7 @@ FLOWS = [ "dialogflow", "directv", "doorbird", + "dsmr", "dunehd", "dynalite", "eafm", diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index edb3810e24f..70f2b16e0b0 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,18 +1,233 @@ """Test the DSMR config flow.""" import asyncio from itertools import chain, repeat -from unittest.mock import DEFAULT, AsyncMock, patch +import os +from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch, sentinel import serial +import serial.tools.list_ports from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.dsmr import DOMAIN +from homeassistant.components.dsmr import DOMAIN, config_flow from tests.common import MockConfigEntry SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} +def com_port(): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/dev/ttyUSB1234" + port.description = "Some serial port" + + return port + + +async def test_setup_network(hass, dsmr_connection_send_validate_fixture): + """Test we can setup network.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Network"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_network" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "10.10.0.1", "port": 1234, "dsmr_version": "2.2"}, + ) + + entry_data = { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + } + + assert result["type"] == "create_entry" + assert result["title"] == "10.10.0.1:1234" + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial(com_mock, hass, dsmr_connection_send_validate_fixture): + """Test we can setup serial.""" + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + ) + + entry_data = { + "port": port.device, + "dsmr_version": "2.2", + } + + assert result["type"] == "create_entry" + assert result["title"] == port.device + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_manual( + com_mock, hass, dsmr_connection_send_validate_fixture +): + """Test we can setup serial with manual entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": "Enter Manually", "dsmr_version": "2.2"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": "/dev/ttyUSB0"} + ) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + } + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_fixture): + """Test failed serial connection.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + await setup.async_setup_component(hass, "persistent_notification", {}) + + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # override the mock to have it fail the first time and succeed after + first_fail_connection_factory = AsyncMock( + return_value=(transport, protocol), + side_effect=chain([serial.serialutil.SerialException], repeat(DEFAULT)), + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch( + "homeassistant.components.dsmr.config_flow.create_dsmr_reader", + first_fail_connection_factory, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_wrong_telegram( + com_mock, hass, dsmr_connection_send_validate_fixture +): + """Test failed telegram data.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + await setup.async_setup_component(hass, "persistent_notification", {}) + + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + protocol.telegram = {} + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {"base": "cannot_communicate"} + + async def test_import_usb(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -265,3 +480,50 @@ async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): assert result["type"] == "create_entry" assert result["title"] == "/dev/ttyUSB0" assert result["data"] == {**entry_data, **SERIAL_DATA} + + +def test_get_serial_by_id_no_dir(): + """Test serial by id conversion if there's no /dev/serial/by-id.""" + p1 = patch("os.path.isdir", MagicMock(return_value=False)) + p2 = patch("os.scandir") + with p1 as is_dir_mock, p2 as scan_mock: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 0 + + +def test_get_serial_by_id(): + """Test serial by id conversion.""" + p1 = patch("os.path.isdir", MagicMock(return_value=True)) + p2 = patch("os.scandir") + + def _realpath(path): + if path is sentinel.matched_link: + return sentinel.path + return sentinel.serial_link_path + + p3 = patch("os.path.realpath", side_effect=_realpath) + with p1 as is_dir_mock, p2 as scan_mock, p3: + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.path + assert is_dir_mock.call_count == 1 + assert scan_mock.call_count == 1 + + entry1 = MagicMock(spec_set=os.DirEntry) + entry1.is_symlink.return_value = True + entry1.path = sentinel.some_path + + entry2 = MagicMock(spec_set=os.DirEntry) + entry2.is_symlink.return_value = False + entry2.path = sentinel.other_path + + entry3 = MagicMock(spec_set=os.DirEntry) + entry3.is_symlink.return_value = True + entry3.path = sentinel.matched_link + + scan_mock.return_value = [entry1, entry2, entry3] + res = config_flow.get_serial_by_id(sentinel.path) + assert res is sentinel.matched_link + assert is_dir_mock.call_count == 2 + assert scan_mock.call_count == 2 From 74db49fae471434b93a394990cee5d05c0e6f150 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 24 Jun 2021 10:54:04 +0200 Subject: [PATCH 499/750] Add KNX select entity (#52026) * select entity for knx * validate select options * lint * phytonify * Tweak Co-authored-by: Franck Nijhof --- homeassistant/components/knx/__init__.py | 2 + homeassistant/components/knx/const.py | 3 +- homeassistant/components/knx/schema.py | 61 +++++++++++++ homeassistant/components/knx/select.py | 104 +++++++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/knx/select.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 613426aaa8f..7b6ccf31b17 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -51,6 +51,7 @@ from .schema import ( NotifySchema, NumberSchema, SceneSchema, + SelectSchema, SensorSchema, SwitchSchema, WeatherSchema, @@ -96,6 +97,7 @@ CONFIG_SCHEMA = vol.Schema( **NotifySchema.platform_node(), **NumberSchema.platform_node(), **SceneSchema.platform_node(), + **SelectSchema.platform_node(), **SensorSchema.platform_node(), **SwitchSchema.platform_node(), **WeatherSchema.platform_node(), diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 08d431b0cff..b1cf80332a7 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -32,7 +32,7 @@ CONF_KNX_INDIVIDUAL_ADDRESS: Final = "individual_address" CONF_KNX_ROUTING: Final = "routing" CONF_KNX_TUNNELING: Final = "tunneling" CONF_RESET_AFTER: Final = "reset_after" -CONF_RESPOND_TO_READ = "respond_to_read" +CONF_RESPOND_TO_READ: Final = "respond_to_read" CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" @@ -59,6 +59,7 @@ class SupportedPlatforms(Enum): NOTIFY = "notify" NUMBER = "number" SCENE = "scene" + SELECT = "select" SENSOR = "sensor" SWITCH = "switch" WEATHER = "weather" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 37730010104..f863efec685 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -116,6 +116,33 @@ def numeric_type_validator(value: Any) -> str | int: raise vol.Invalid(f"value '{value}' is not a valid numeric sensor type.") +def select_options_sub_validator(entity_config: OrderedDict) -> OrderedDict: + """Validate a select entity options configuration.""" + options_seen = set() + payloads_seen = set() + payload_length = entity_config[SelectSchema.CONF_PAYLOAD_LENGTH] + if payload_length == 0: + max_payload = 0x3F + else: + max_payload = 256 ** payload_length - 1 + + for opt in entity_config[SelectSchema.CONF_OPTIONS]: + option = opt[SelectSchema.CONF_OPTION] + payload = opt[SelectSchema.CONF_PAYLOAD] + if payload > max_payload: + raise vol.Invalid( + f"'payload: {payload}' for 'option: {option}' exceeds possible" + f" maximum of 'payload_length: {payload_length}': {max_payload}" + ) + if option in options_seen: + raise vol.Invalid(f"duplicate item for 'option' not allowed: {option}") + options_seen.add(option) + if payload in payloads_seen: + raise vol.Invalid(f"duplicate item for 'payload' not allowed: {payload}") + payloads_seen.add(payload) + return entity_config + + def sensor_type_validator(value: Any) -> str | int: """Validate that value is parsable as sensor type.""" if isinstance(value, (str, int)) and DPTBase.parse_transcoder(value) is not None: @@ -617,6 +644,40 @@ class SceneSchema(KNXPlatformSchema): ) +class SelectSchema(KNXPlatformSchema): + """Voluptuous schema for KNX selects.""" + + PLATFORM_NAME = SupportedPlatforms.SELECT.value + + CONF_OPTION = "option" + CONF_OPTIONS = "options" + CONF_PAYLOAD = "payload" + CONF_PAYLOAD_LENGTH = "payload_length" + DEFAULT_NAME = "KNX Select" + + ENTITY_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Required(CONF_PAYLOAD_LENGTH): vol.All( + vol.Coerce(int), vol.Range(min=0, max=14) + ), + vol.Required(CONF_OPTIONS): [ + { + vol.Required(CONF_OPTION): vol.Coerce(str), + vol.Required(CONF_PAYLOAD): cv.positive_int, + } + ], + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + } + ), + select_options_sub_validator, + ) + + class SensorSchema(KNXPlatformSchema): """Voluptuous schema for KNX sensors.""" diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py new file mode 100644 index 00000000000..a545cd3f46b --- /dev/null +++ b/homeassistant/components/knx/select.py @@ -0,0 +1,104 @@ +"""Support for KNX/IP select entities.""" +from __future__ import annotations + +from xknx import XKNX +from xknx.devices import Device as XknxDevice, RawValue + +from homeassistant.components.select import SelectEntity +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ( + CONF_RESPOND_TO_READ, + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + DOMAIN, + KNX_ADDRESS, +) +from .knx_entity import KnxEntity +from .schema import SelectSchema + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up number entities for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + + async_add_entities( + KNXSelect(xknx, entity_config) for entity_config in platform_config + ) + + +def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: + """Return a KNX RawValue to be used within XKNX.""" + return RawValue( + xknx, + name=config[CONF_NAME], + payload_length=config[SelectSchema.CONF_PAYLOAD_LENGTH], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], + sync_state=config[CONF_SYNC_STATE], + ) + + +class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): + """Representation of a KNX number.""" + + def __init__(self, xknx: XKNX, config: ConfigType) -> None: + """Initialize a KNX number.""" + self._device: RawValue + super().__init__(_create_raw_value(xknx, config)) + self._unique_id = f"{self._device.remote_value.group_address}" + + self._option_payloads: dict[str, int] = { + option[SelectSchema.CONF_OPTION]: option[SelectSchema.CONF_PAYLOAD] + for option in config[SelectSchema.CONF_OPTIONS] + } + self._attr_options = list(self._option_payloads) + self._attr_current_option = None + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if not self._device.remote_value.readable and ( + last_state := await self.async_get_last_state() + ): + if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + await self._device.remote_value.update_value( + self._option_payloads.get(last_state.state) + ) + + async def after_update_callback(self, device: XknxDevice) -> None: + """Call after device was updated.""" + self._attr_current_option = self.option_from_payload( + self._device.remote_value.value + ) + await super().after_update_callback(device) + + def option_from_payload(self, payload: int | None) -> str | None: + """Return the option a given payload is assigned to.""" + try: + return next( + key for key, value in self._option_payloads.items() if value == payload + ) + except StopIteration: + return None + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if payload := self._option_payloads.get(option): + await self._device.set(payload) + return + raise ValueError(f"Invalid option for {self.entity_id}: {option}") From ff8b96c65d491418fd2cb70de8c2b17c2fb851d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 24 Jun 2021 11:10:21 +0200 Subject: [PATCH 500/750] Remove YAML configuration import from Sony Bravia TV (#52141) --- .../components/braviatv/config_flow.py | 24 ----- .../components/braviatv/media_player.py | 63 +------------ tests/components/braviatv/test_config_flow.py | 91 +------------------ 3 files changed, 4 insertions(+), 174 deletions(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 588014ebd3c..0813a3e52c5 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,6 +1,5 @@ """Adds config flow for Bravia TV integration.""" import ipaddress -import logging import re from bravia_tv import BraviaRC @@ -22,8 +21,6 @@ from .const import ( NICKNAME, ) -_LOGGER = logging.getLogger(__name__) - def host_valid(host): """Return True if hostname or IP address is valid.""" @@ -75,27 +72,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) - async def async_step_import(self, user_input=None): - """Handle configuration by yaml file.""" - self.host = user_input[CONF_HOST] - self.braviarc = BraviaRC(self.host) - - try: - await self.init_device(user_input[CONF_PIN]) - except CannotConnect: - _LOGGER.error("Import aborted, cannot connect to %s", self.host) - return self.async_abort(reason="cannot_connect") - except NoIPControl: - _LOGGER.error("IP Control is disabled in the TV settings") - return self.async_abort(reason="no_ip_control") - except ModelNotSupported: - _LOGGER.error("Import aborted, your TV is not supported") - return self.async_abort(reason="unsupported_model") - - user_input[CONF_MAC] = self.mac - - return self.async_create_entry(title=self.title, data=user_input) - async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 2773a185bb1..dda5b005497 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,13 +1,5 @@ """Support for interface with a Bravia TV.""" -import logging - -import voluptuous as vol - -from homeassistant.components.media_player import ( - DEVICE_CLASS_TV, - PLATFORM_SCHEMA, - MediaPlayerEntity, -) +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -21,22 +13,10 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PIN, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.json import load_json -from .const import ATTR_MANUFACTURER, BRAVIA_CONFIG_FILE, DEFAULT_NAME, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN SUPPORT_BRAVIA = ( SUPPORT_PAUSE @@ -52,43 +32,6 @@ SUPPORT_BRAVIA = ( | SUPPORT_STOP ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Bravia TV platform.""" - host = config[CONF_HOST] - - bravia_config_file_path = hass.config.path(BRAVIA_CONFIG_FILE) - bravia_config = await hass.async_add_executor_job( - load_json, bravia_config_file_path - ) - if not bravia_config: - _LOGGER.error( - "Configuration import failed, there is no bravia.conf file in the configuration folder" - ) - return - - while bravia_config: - # Import a configured TV - host_ip, host_config = bravia_config.popitem() - if host_ip == host: - pin = host_config[CONF_PIN] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: host, CONF_PIN: pin}, - ) - ) - return - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Bravia TV Media Player from a config_entry.""" diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 36c7ae9955a..7ac9439e711 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -5,7 +5,7 @@ from bravia_tv.braviarc import NoIPControl 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.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from tests.common import MockConfigEntry @@ -31,9 +31,6 @@ BRAVIA_SOURCE_LIST = { "AV/Component": "extInput:component?port=1", } -IMPORT_CONFIG_HOSTNAME = {CONF_HOST: "bravia-host", CONF_PIN: "1234"} -IMPORT_CONFIG_IP = {CONF_HOST: "10.10.10.12", CONF_PIN: "1234"} - async def test_show_form(hass): """Test that the form is served with no input.""" @@ -45,92 +42,6 @@ async def test_show_form(hass): assert result["step_id"] == SOURCE_USER -async def test_import(hass): - """Test that the import works.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO - ), patch( - "homeassistant.components.braviatv.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == "very_unique_string" - assert result["title"] == "TV-Model" - assert result["data"] == { - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - } - - -async def test_import_cannot_connect(hass): - """Test that errors are shown when cannot connect to the host during import.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=False - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_model_unsupported(hass): - """Test that errors are shown when the TV is not supported during import.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "unsupported_model" - - -async def test_import_no_ip_control(hass): - """Test that errors are shown when IP Control is disabled on the TV during import.""" - with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "no_ip_control" - - -async def test_import_duplicate_error(hass): - """Test that errors are shown when duplicates are added during import.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_user_invalid_host(hass): """Test that errors are shown when the host is invalid.""" result = await hass.config_entries.flow.async_init( From 780d538bb0cfedd6bef87e421110129e44141015 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 11:23:20 +0200 Subject: [PATCH 501/750] DSMR: Adding myself to the codeowners (#52144) --- CODEOWNERS | 2 +- homeassistant/components/dsmr/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 891f0e37f72..8d22525b34b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -118,7 +118,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 @bdraco -homeassistant/components/dsmr/* @Robbie1221 +homeassistant/components/dsmr/* @Robbie1221 @frenck homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dunehd/* @bieniu homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 38df6f733cd..df738724ac0 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -3,7 +3,7 @@ "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": ["dsmr_parser==0.29"], - "codeowners": ["@Robbie1221"], + "codeowners": ["@Robbie1221", "@frenck"], "config_flow": true, "iot_class": "local_push" } From e21325b975fdabaa259b800d1b98a8d78f04fd08 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 24 Jun 2021 11:24:38 +0200 Subject: [PATCH 502/750] Fix missing azure event hub instance name (#52049) --- .../components/azure_event_hub/__init__.py | 30 ++++++++++--------- .../components/azure_event_hub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 0c5ae2b81b8..1c9add1bd8b 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -23,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.helpers.event import async_call_later from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.typing import ConfigType from .const import ( ADDITIONAL_ARGS, @@ -43,9 +44,9 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { + vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, vol.Exclusive(CONF_EVENT_HUB_CON_STRING, "setup_methods"): cv.string, vol.Exclusive(CONF_EVENT_HUB_NAMESPACE, "setup_methods"): cv.string, - vol.Optional(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, vol.Optional(CONF_EVENT_HUB_SAS_POLICY): cv.string, vol.Optional(CONF_EVENT_HUB_SAS_KEY): cv.string, vol.Optional(CONF_SEND_INTERVAL, default=5): cv.positive_int, @@ -61,20 +62,23 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, yaml_config): +async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Azure EH component.""" config = yaml_config[DOMAIN] if config.get(CONF_EVENT_HUB_CON_STRING): - client_args = {"conn_str": config[CONF_EVENT_HUB_CON_STRING]} + client_args = { + "conn_str": config[CONF_EVENT_HUB_CON_STRING], + "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], + } conn_str_client = True else: client_args = { "fully_qualified_namespace": f"{config[CONF_EVENT_HUB_NAMESPACE]}.servicebus.windows.net", + "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], "credential": EventHubSharedKeyCredential( policy=config[CONF_EVENT_HUB_SAS_POLICY], key=config[CONF_EVENT_HUB_SAS_KEY], ), - "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], } conn_str_client = False @@ -115,7 +119,7 @@ class AzureEventHub: self._next_send_remover = None self.shutdown = False - async def async_start(self): + async def async_start(self) -> None: """Start the recorder, suppress logging and register the callbacks and do the first send after five seconds, to capture the startup events.""" # suppress the INFO and below logging on the underlying packages, they are very verbose, even at INFO logging.getLogger("uamqp").setLevel(logging.WARNING) @@ -128,7 +132,7 @@ class AzureEventHub: # schedule the first send after 10 seconds to capture startup events, after that each send will schedule the next after the interval. self._next_send_remover = async_call_later(self.hass, 10, self.async_send) - async def async_shutdown(self, _: Event): + async def async_shutdown(self, _: Event) -> None: """Shut down the AEH by queueing None and calling send.""" if self._next_send_remover: self._next_send_remover() @@ -137,14 +141,13 @@ class AzureEventHub: await self.queue.put((3, (time.monotonic(), None))) await self.async_send(None) - async def async_listen(self, event: Event): + async def async_listen(self, event: Event) -> None: """Listen for new messages on the bus and queue them for AEH.""" await self.queue.put((2, (time.monotonic(), event))) - async def async_send(self, _): + async def async_send(self, _) -> None: """Write preprocessed events to eventhub, with retry.""" - client = self._get_client() - async with client: + async with self._get_client() as client: while not self.queue.empty(): data_batch, dequeue_count = await self.fill_batch(client) _LOGGER.debug( @@ -160,14 +163,13 @@ class AzureEventHub: finally: for _ in range(dequeue_count): self.queue.task_done() - await client.close() if not self.shutdown: self._next_send_remover = async_call_later( self.hass, self._send_interval, self.async_send ) - async def fill_batch(self, client): + async def fill_batch(self, client) -> None: """Return a batch of events formatted for writing. Uses get_nowait instead of await get, because the functions batches and doesn't wait for each single event, the send function is called. @@ -205,7 +207,7 @@ class AzureEventHub: return event_batch, dequeue_count - def _event_to_filtered_event_data(self, event: Event): + def _event_to_filtered_event_data(self, event: Event) -> None: """Filter event states and create EventData object.""" state = event.data.get("new_state") if ( @@ -216,7 +218,7 @@ class AzureEventHub: return None return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) - def _get_client(self): + def _get_client(self) -> EventHubProducerClient: """Get a Event Producer Client.""" if self._conn_str_client: return EventHubProducerClient.from_connection_string( diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index b570f11e28f..9c63af35340 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_event_hub", "name": "Azure Event Hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", - "requirements": ["azure-eventhub==5.1.0"], + "requirements": ["azure-eventhub==5.5.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 887cef869d3..fb24a7b78c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -331,7 +331,7 @@ av==8.0.3 axis==44 # homeassistant.components.azure_event_hub -azure-eventhub==5.1.0 +azure-eventhub==5.5.0 # homeassistant.components.azure_service_bus azure-servicebus==0.50.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e30b7819f2..11acc12e51e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ av==8.0.3 axis==44 # homeassistant.components.azure_event_hub -azure-eventhub==5.1.0 +azure-eventhub==5.5.0 # homeassistant.components.homekit base36==0.1.1 From 17357bf57527cb53090f59be951b925d41e11ae4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 11:25:19 +0200 Subject: [PATCH 503/750] DSMR: Small cleanup; use entity class attributes (#52143) --- homeassistant/components/dsmr/sensor.py | 51 +++++++------------------ 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 00cf5d21e86..d023c71592b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -23,7 +23,6 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from .const import ( @@ -289,17 +288,21 @@ async def async_setup_entry( class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" + _attr_should_poll = False + def __init__(self, name, device_name, device_serial, obis, config, force_update): """Initialize entity.""" - self._name = name self._obis = obis self._config = config self.telegram = {} - self._device_name = device_name - self._device_serial = device_serial - self._force_update = force_update - self._unique_id = f"{device_serial}_{name}".replace(" ", "_") + self._attr_name = name + self._attr_force_update = force_update + self._attr_unique_id = f"{device_serial}_{name}".replace(" ", "_") + self._attr_device_info = { + "identifiers": {(DOMAIN, device_serial)}, + "name": device_name, + } @callback def update_data(self, telegram): @@ -318,21 +321,16 @@ class DSMREntity(SensorEntity): dsmr_object = self.telegram[self._obis] return getattr(dsmr_object, attribute, None) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def icon(self): """Icon to use in the frontend, if any.""" - if "Sags" in self._name or "Swells" in self.name: + if "Sags" in self.name or "Swells" in self.name: return ICON_SWELL_SAG - if "Failure" in self._name: + if "Failure" in self.name: return ICON_POWER_FAILURE - if "Power" in self._name: + if "Power" in self.name: return ICON_POWER - if "Gas" in self._name: + if "Gas" in self.name: return ICON_GAS @property @@ -358,29 +356,6 @@ class DSMREntity(SensorEntity): """Return the unit of measurement of this entity, if any.""" return self.get_dsmr_object_attr("unit") - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._device_serial)}, - "name": self._device_name, - } - - @property - def force_update(self): - """Force update.""" - return self._force_update - - @property - def should_poll(self): - """Disable polling.""" - return False - @staticmethod def translate_tariff(value, dsmr_version): """Convert 2/1 to normal/low depending on DSMR version.""" From fbdd6a9d95ac85a383e1703693bd4b1e32015a91 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 12:53:16 +0200 Subject: [PATCH 504/750] DSMR: Typing cleanup in init & config flow (#52145) --- homeassistant/components/dsmr/__init__.py | 4 +- homeassistant/components/dsmr/config_flow.py | 101 ++++++++++--------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - tests/components/dsmr/test_config_flow.py | 12 +-- 5 files changed, 59 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 61c7660fc69..3ebac7b42fc 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" task = hass.data[DOMAIN][entry.entry_id][DATA_TASK] listener = hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] @@ -40,6 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 2312cad215b..1ddbdb480c1 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -10,13 +10,17 @@ from typing import Any from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.objects import DSMRObject import serial import serial.tools.list_ports import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_DSMR_VERSION, @@ -35,33 +39,34 @@ CONF_MANUAL_PATH = "Enter Manually" class DSMRConnection: """Test the connection to DSMR and receive telegram to read serial ids.""" - def __init__(self, host, port, dsmr_version): + def __init__(self, host: str | None, port: int, dsmr_version: str) -> None: """Initialize.""" self._host = host self._port = port self._dsmr_version = dsmr_version - self._telegram = {} + self._telegram: dict[str, DSMRObject] = {} + self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER if dsmr_version == "5L": self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER - else: - self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER - def equipment_identifier(self): + def equipment_identifier(self) -> str | None: """Equipment identifier.""" if self._equipment_identifier in self._telegram: dsmr_object = self._telegram[self._equipment_identifier] return getattr(dsmr_object, "value", None) + return None - def equipment_identifier_gas(self): + def equipment_identifier_gas(self) -> str | None: """Equipment identifier gas.""" if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram: dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS] return getattr(dsmr_object, "value", None) + return None async def validate_connect(self, hass: core.HomeAssistant) -> bool: """Test if we can validate connection with the device.""" - def update_telegram(telegram): + def update_telegram(telegram: dict[str, DSMRObject]) -> None: if self._equipment_identifier in telegram: self._telegram = telegram transport.close() @@ -101,7 +106,9 @@ class DSMRConnection: return True -async def _validate_dsmr_connection(hass: core.HomeAssistant, data): +async def _validate_dsmr_connection( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str | None]: """Validate the user input allows us to connect.""" conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION]) @@ -115,26 +122,22 @@ async def _validate_dsmr_connection(hass: core.HomeAssistant, data): if equipment_identifier is None: raise CannotCommunicate - info = { + return { CONF_SERIAL_ID: equipment_identifier, CONF_SERIAL_ID_GAS: equipment_identifier_gas, } - return info - class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for DSMR.""" VERSION = 1 - def __init__(self): - """Initialize flow instance.""" - self._dsmr_version = None + _dsmr_version: str | None = None @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> DSMROptionFlowHandler: """Get the options flow for this handler.""" return DSMROptionFlowHandler(config_entry) @@ -144,7 +147,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host: str = None, updates: dict[Any, Any] | None = None, reload_on_update: bool = True, - ): + ) -> FlowResult | None: """Test if host and port are already configured.""" for entry in self._async_current_entries(): if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port: @@ -168,9 +171,10 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when user initializes a integration.""" - errors = {} if user_input is not None: user_selection = user_input[CONF_TYPE] if user_selection == "Serial": @@ -181,15 +185,15 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): list_of_types = ["Serial", "Network"] schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) - return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + return self.async_show_form(step_id="user", data_schema=schema) - async def async_step_setup_network(self, user_input=None): + async def async_step_setup_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when setting up network configuration.""" - errors = {} - + errors: dict[str, str] = {} if user_input is not None: data = await self.async_validate_dsmr(user_input, errors) - if not errors: return self.async_create_entry( title=f"{data[CONF_HOST]}:{data[CONF_PORT]}", data=data @@ -208,10 +212,11 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial(self, user_input=None): + async def async_step_setup_serial( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when setting up serial configuration.""" - errors = {} - + errors: dict[str, str] = {} if user_input is not None: user_selection = user_input[CONF_PORT] if user_selection == CONF_MANUAL_PATH: @@ -228,18 +233,15 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } data = await self.async_validate_dsmr(validate_data, errors) - if not errors: return self.async_create_entry(title=data[CONF_PORT], data=data) ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - list_of_ports = {} - for port in ports: - list_of_ports[ - port.device - ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( - f" - {port.manufacturer}" if port.manufacturer else "" - ) + list_of_ports = { + port.device: f"{port}, s/n: {port.serial_number or 'n/a'}" + + (f" - {port.manufacturer}" if port.manufacturer else "") + for port in ports + } list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH schema = vol.Schema( @@ -254,18 +256,18 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial_manual_path(self, user_input=None): + async def async_step_setup_serial_manual_path( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Select path manually.""" - errors = {} - if user_input is not None: validate_data = { CONF_PORT: user_input[CONF_PORT], CONF_DSMR_VERSION: self._dsmr_version, } + errors: dict[str, str] = {} data = await self.async_validate_dsmr(validate_data, errors) - if not errors: return self.async_create_entry(title=data[CONF_PORT], data=data) @@ -273,10 +275,11 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="setup_serial_manual_path", data_schema=schema, - errors=errors, ) - async def async_validate_dsmr(self, input_data, errors): + async def async_validate_dsmr( + self, input_data: dict[str, Any], errors: dict[str, str] + ) -> dict[str, Any]: """Validate dsmr connection and create data.""" data = input_data @@ -294,7 +297,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return data - async def async_step_import(self, import_config=None): + async def async_step_import(self, import_config: ConfigType) -> FlowResult: """Handle the initial step.""" host = import_config.get(CONF_HOST) port = import_config[CONF_PORT] @@ -310,11 +313,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except CannotCommunicate: return self.async_abort(reason="cannot_communicate") - if host is not None: - name = f"{host}:{port}" - else: - name = port - + name = f"{host}:{port}" if host is not None else port data = {**import_config, **info} await self.async_set_unique_id(info[CONF_SERIAL_ID]) @@ -326,11 +325,13 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class DSMROptionFlowHandler(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry): + def __init__(self, entry: ConfigEntry) -> None: """Initialize options flow.""" - self.config_entry = config_entry + self.entry = entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -341,7 +342,7 @@ class DSMROptionFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_TIME_BETWEEN_UPDATE, - default=self.config_entry.options.get( + default=self.entry.options.get( CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE ), ): vol.All(vol.Coerce(int), vol.Range(min=0)), diff --git a/mypy.ini b/mypy.ini index 50257141179..505d02b1111 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1053,9 +1053,6 @@ ignore_errors = true [mypy-homeassistant.components.doorbird.*] ignore_errors = true -[mypy-homeassistant.components.dsmr.*] -ignore_errors = true - [mypy-homeassistant.components.dynalite.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b934bed3bc1..21600257ffb 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -46,7 +46,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", "homeassistant.components.doorbird.*", - "homeassistant.components.dsmr.*", "homeassistant.components.dynalite.*", "homeassistant.components.eafm.*", "homeassistant.components.edl21.*", diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 70f2b16e0b0..006893a81e8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -34,7 +34,7 @@ async def test_setup_network(hass, dsmr_connection_send_validate_fixture): assert result["type"] == "form" assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["errors"] is None result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -73,7 +73,7 @@ async def test_setup_serial(com_mock, hass, dsmr_connection_send_validate_fixtur assert result["type"] == "form" assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["errors"] is None result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -110,7 +110,7 @@ async def test_setup_serial_manual( assert result["type"] == "form" assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["errors"] is None result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -127,7 +127,7 @@ async def test_setup_serial_manual( assert result["type"] == "form" assert result["step_id"] == "setup_serial_manual_path" - assert result["errors"] == {} + assert result["errors"] is None with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -165,7 +165,7 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f assert result["type"] == "form" assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["errors"] is None result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -208,7 +208,7 @@ async def test_setup_serial_wrong_telegram( assert result["type"] == "form" assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["errors"] is None result = await hass.config_entries.flow.async_configure( result["flow_id"], From 0e5040d917614ddfaf9863103c6de767d2b21cda Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 24 Jun 2021 13:15:42 +0200 Subject: [PATCH 505/750] Add zwave_js options flow to reconfigure server (#51840) --- homeassistant/components/zwave_js/addon.py | 20 + .../components/zwave_js/config_flow.py | 269 ++++++- homeassistant/components/zwave_js/const.py | 2 + .../components/zwave_js/strings.json | 46 ++ .../components/zwave_js/translations/en.json | 50 ++ tests/components/zwave_js/conftest.py | 53 +- tests/components/zwave_js/test_config_flow.py | 753 +++++++++++++++++- 7 files changed, 1176 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index ff74b5d5a44..a0caaa15488 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -12,6 +12,7 @@ from homeassistant.components.hassio import ( async_get_addon_discovery_info, async_get_addon_info, async_install_addon, + async_restart_addon, async_set_addon_options, async_start_addon, async_stop_addon, @@ -89,6 +90,7 @@ class AddonManager: """Set up the add-on manager.""" self._hass = hass self._install_task: asyncio.Task | None = None + self._restart_task: asyncio.Task | None = None self._start_task: asyncio.Task | None = None self._update_task: asyncio.Task | None = None @@ -222,6 +224,11 @@ class AddonManager: """Start the Z-Wave JS add-on.""" await async_start_addon(self._hass, ADDON_SLUG) + @api_error("Failed to restart the Z-Wave JS add-on") + async def async_restart_addon(self) -> None: + """Restart the Z-Wave JS add-on.""" + await async_restart_addon(self._hass, ADDON_SLUG) + @callback def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task: """Schedule a task that starts the Z-Wave JS add-on. @@ -235,6 +242,19 @@ class AddonManager: ) return self._start_task + @callback + def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that restarts the Z-Wave JS add-on. + + Only schedule a new restart task if the there's no running task. + """ + if not self._restart_task or self._restart_task.done(): + LOGGER.info("Restarting Z-Wave JS add-on") + self._restart_task = self._async_schedule_addon_operation( + self.async_restart_addon, catch_error=catch_error + ) + return self._restart_task + @api_error("Failed to stop the Z-Wave JS add-on") async def async_stop_addon(self) -> None: """Stop the Z-Wave JS add-on.""" diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 4dd0a084711..ced8b2c68cb 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -23,9 +23,12 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from . import disconnect_client from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager from .const import ( CONF_ADDON_DEVICE, + CONF_ADDON_EMULATE_HARDWARE, + CONF_ADDON_LOG_LEVEL, CONF_ADDON_NETWORK_KEY, CONF_INTEGRATION_CREATED_ADDON, CONF_NETWORK_KEY, @@ -41,8 +44,25 @@ TITLE = "Z-Wave JS" ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 4 +CONF_EMULATE_HARDWARE = "emulate_hardware" +CONF_LOG_LEVEL = "log_level" SERVER_VERSION_TIMEOUT = 10 +ADDON_LOG_LEVELS = { + "error": "Error", + "warn": "Warn", + "info": "Info", + "verbose": "Verbose", + "debug": "Debug", + "silly": "Silly", +} +ADDON_USER_INPUT_MAP = { + CONF_ADDON_DEVICE: CONF_USB_PATH, + CONF_ADDON_NETWORK_KEY: CONF_NETWORK_KEY, + CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL, + CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE, +} + ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) @@ -52,6 +72,12 @@ def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: return vol.Schema({vol.Required(CONF_URL, default=default_url): str}) +def get_on_supervisor_schema(user_input: dict[str, Any]) -> vol.Schema: + """Return a schema for the on Supervisor step.""" + default_use_addon = user_input[CONF_USE_ADDON] + return vol.Schema({vol.Optional(CONF_USE_ADDON, default=default_use_addon): bool}) + + async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: """Validate if the user input allows us to connect.""" ws_address = user_input[CONF_URL] @@ -89,6 +115,7 @@ class BaseZwaveJSFlow(FlowHandler): self.network_key: str | None = None self.usb_path: str | None = None self.ws_address: str | None = None + self.restart_addon: bool = False # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False self.install_task: asyncio.Task | None = None @@ -159,7 +186,10 @@ class BaseZwaveJSFlow(FlowHandler): addon_manager: AddonManager = get_addon_manager(self.hass) self.version_info = None try: - await addon_manager.async_schedule_start_addon() + if self.restart_addon: + await addon_manager.async_schedule_restart_addon() + else: + await addon_manager.async_schedule_start_addon() # Sleep some seconds to let the add-on start properly before connecting. for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS): await asyncio.sleep(ADDON_SETUP_TIMEOUT) @@ -262,6 +292,14 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): """Return the correct flow manager.""" return self.hass.config_entries.flow + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Return the options flow.""" + return OptionsFlowHandler(config_entry) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -445,6 +483,235 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): ) +class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): + """Handle an options flow for Z-Wave JS.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Set up the options flow.""" + super().__init__() + self.config_entry = config_entry + self.original_addon_config: dict[str, Any] | None = None + self.revert_reason: str | None = None + + @property + def flow_manager(self) -> config_entries.OptionsFlowManager: + """Return the correct flow manager.""" + return self.hass.config_entries.options + + @callback + def _async_update_entry(self, data: dict[str, Any]) -> None: + """Update the config entry with new data.""" + self.hass.config_entries.async_update_entry(self.config_entry, data=data) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if is_hassio(self.hass): + return await self.async_step_on_supervisor() + + return await self.async_step_manual() + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a manual configuration.""" + if user_input is None: + return self.async_show_form( + step_id="manual", + data_schema=get_manual_schema( + {CONF_URL: self.config_entry.data[CONF_URL]} + ), + ) + + errors = {} + + try: + version_info = await validate_input(self.hass, user_input) + except InvalidInput as err: + errors["base"] = err.error + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + if self.config_entry.unique_id != version_info.home_id: + return self.async_abort(reason="different_device") + + # Make sure we disable any add-on handling + # if the controller is reconfigured in a manual step. + self._async_update_entry( + { + **self.config_entry.data, + **user_input, + CONF_USE_ADDON: False, + CONF_INTEGRATION_CREATED_ADDON: False, + } + ) + + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + return self.async_create_entry(title=TITLE, data={}) + + return self.async_show_form( + step_id="manual", data_schema=get_manual_schema(user_input), errors=errors + ) + + async def async_step_on_supervisor( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle logic when on Supervisor host.""" + if user_input is None: + return self.async_show_form( + step_id="on_supervisor", + data_schema=get_on_supervisor_schema( + {CONF_USE_ADDON: self.config_entry.data.get(CONF_USE_ADDON, True)} + ), + ) + if not user_input[CONF_USE_ADDON]: + return await self.async_step_manual() + + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_addon() + + return await self.async_step_configure_addon() + + async def async_step_configure_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ask for config for Z-Wave JS add-on.""" + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options + + if user_input is not None: + self.network_key = user_input[CONF_NETWORK_KEY] + self.usb_path = user_input[CONF_USB_PATH] + + new_addon_config = { + **addon_config, + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], + CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE], + } + + if new_addon_config != addon_config: + if addon_info.state == AddonState.RUNNING: + self.restart_addon = True + # Copy the add-on config to keep the objects separate. + self.original_addon_config = dict(addon_config) + await self._async_set_addon_config(new_addon_config) + + if addon_info.state == AddonState.RUNNING and not self.restart_addon: + return await self.async_step_finish_addon_setup() + + if ( + self.config_entry.data.get(CONF_USE_ADDON) + and self.config_entry.state == config_entries.ConfigEntryState.LOADED + ): + # Disconnect integration before restarting add-on. + await disconnect_client(self.hass, self.config_entry) + + return await self.async_step_start_addon() + + usb_path = addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, self.network_key or "") + log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") + emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) + + data_schema = vol.Schema( + { + vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=network_key): str, + vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In( + ADDON_LOG_LEVELS + ), + vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool, + } + ) + + return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + + async def async_step_start_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Add-on start failed.""" + return await self.async_revert_addon_config(reason="addon_start_failed") + + async def async_step_finish_addon_setup( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Prepare info needed to complete the config entry update. + + Get add-on discovery info and server version info. + Check for same unique id and abort if not the same unique id. + """ + if self.revert_reason: + self.original_addon_config = None + reason = self.revert_reason + self.revert_reason = None + return await self.async_revert_addon_config(reason=reason) + + if not self.ws_address: + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + + if not self.version_info: + try: + self.version_info = await async_get_version_info( + self.hass, self.ws_address + ) + except CannotConnect: + return await self.async_revert_addon_config(reason="cannot_connect") + + if self.config_entry.unique_id != self.version_info.home_id: + return await self.async_revert_addon_config(reason="different_device") + + self._async_update_entry( + { + **self.config_entry.data, + CONF_URL: self.ws_address, + CONF_USB_PATH: self.usb_path, + CONF_NETWORK_KEY: self.network_key, + CONF_USE_ADDON: True, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + } + ) + # Always reload entry since we may have disconnected the client. + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + return self.async_create_entry(title=TITLE, data={}) + + async def async_revert_addon_config(self, reason: str) -> FlowResult: + """Abort the options flow. + + If the add-on options have been changed, revert those and restart add-on. + """ + # If reverting the add-on options failed, abort immediately. + if self.revert_reason: + _LOGGER.error( + "Failed to revert add-on options before aborting flow, reason: %s", + reason, + ) + + if self.revert_reason or not self.original_addon_config: + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + return self.async_abort(reason=reason) + + self.revert_reason = reason + addon_config_input = { + ADDON_USER_INPUT_MAP[addon_key]: addon_val + for addon_key, addon_val in self.original_addon_config.items() + } + _LOGGER.debug("Reverting add-on options, reason: %s", reason) + return await self.async_step_configure_addon(addon_config_input) + + class CannotConnect(exceptions.HomeAssistantError): """Indicate connection error.""" diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index d73f05c4c47..9e6e37b4ee7 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -2,6 +2,8 @@ import logging CONF_ADDON_DEVICE = "device" +CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" +CONF_ADDON_LOG_LEVEL = "log_level" CONF_ADDON_NETWORK_KEY = "network_key" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" CONF_NETWORK_KEY = "network_key" diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index eb13ad512e3..b942a75b27a 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -47,5 +47,51 @@ "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." } + }, + "options": { + "step": { + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "title": "Select connection method", + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" } + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "configure_addon": { + "title": "Enter the Z-Wave JS add-on configuration", + "data": { + "usb_path": "[%key:common::config_flow::data::usb_path%]", + "network_key": "Network Key", + "log_level": "Log level", + "emulate_hardware": "Emulate Hardware" + } + }, + "start_addon": { "title": "The Z-Wave JS add-on is starting." } + }, + "error": { + "invalid_ws_url": "Invalid websocket URL", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "addon_info_failed": "Failed to get Z-Wave JS add-on info.", + "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_start_failed": "Failed to start the Z-Wave JS add-on.", + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device." + }, + "progress": { + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." + } } } diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 101942dc717..27cafb6af6e 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "addon_info_failed": "Failed to get Z-Wave JS add-on info.", + "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_start_failed": "Failed to start the Z-Wave JS add-on.", + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "different_device": "The connected USB device is not the same as previously configured for this config entry. Please instead create a new config entry for the new device." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "Unexpected error" + }, + "progress": { + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the Z-Wave JS add-on start completes. This may take some seconds." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulate Hardware", + "log_level": "Log level", + "network_key": "Network Key", + "usb_path": "USB Device Path" + }, + "title": "Enter the Z-Wave JS add-on configuration" + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use the Z-Wave JS Supervisor add-on" + }, + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "title": "Select connection method" + }, + "start_addon": { + "title": "The Z-Wave JS add-on is starting." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index caddbb050a5..f0c69709031 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -60,9 +60,14 @@ def mock_addon_options(addon_info): @pytest.fixture(name="set_addon_options_side_effect") -def set_addon_options_side_effect_fixture(): +def set_addon_options_side_effect_fixture(addon_options): """Return the set add-on options side effect.""" - return None + + async def set_addon_options(hass, slug, options): + """Mock set add-on options.""" + addon_options.update(options["options"]) + + return set_addon_options @pytest.fixture(name="set_addon_options") @@ -75,11 +80,24 @@ def mock_set_addon_options(set_addon_options_side_effect): yield set_options +@pytest.fixture(name="install_addon_side_effect") +def install_addon_side_effect_fixture(addon_info): + """Return the install add-on side effect.""" + + async def install_addon(hass, slug): + """Mock install add-on.""" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0" + + return install_addon + + @pytest.fixture(name="install_addon") -def mock_install_addon(): +def mock_install_addon(install_addon_side_effect): """Mock install add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_install_addon" + "homeassistant.components.zwave_js.addon.async_install_addon", + side_effect=install_addon_side_effect, ) as install_addon: yield install_addon @@ -94,9 +112,14 @@ def mock_update_addon(): @pytest.fixture(name="start_addon_side_effect") -def start_addon_side_effect_fixture(): - """Return the set add-on options side effect.""" - return None +def start_addon_side_effect_fixture(addon_info): + """Return the start add-on options side effect.""" + + async def start_addon(hass, slug): + """Mock start add-on.""" + addon_info.return_value["state"] = "started" + + return start_addon @pytest.fixture(name="start_addon") @@ -118,6 +141,22 @@ def stop_addon_fixture(): yield stop_addon +@pytest.fixture(name="restart_addon_side_effect") +def restart_addon_side_effect_fixture(): + """Return the restart add-on options side effect.""" + return None + + +@pytest.fixture(name="restart_addon") +def mock_restart_addon(restart_addon_side_effect): + """Mock restart add-on.""" + with patch( + "homeassistant.components.zwave_js.addon.async_restart_addon", + side_effect=restart_addon_side_effect, + ) as restart_addon: + yield restart_addon + + @pytest.fixture(name="uninstall_addon") def uninstall_addon_fixture(): """Mock uninstall add-on.""" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 2414d2aea00..7d02c215d45 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -2,6 +2,7 @@ import asyncio from unittest.mock import DEFAULT, call, patch +import aiohttp import pytest from zwave_js_server.version import VersionInfo @@ -19,6 +20,21 @@ ADDON_DISCOVERY_INFO = { } +@pytest.fixture(name="persistent_notification", autouse=True) +async def setup_persistent_notification(hass): + """Set up persistent notification integration.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + +@pytest.fixture(name="setup_entry") +def setup_entry_fixture(): + """Mock entry setup.""" + with patch( + "homeassistant.components.zwave_js.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + @pytest.fixture(name="supervisor") def mock_supervisor_fixture(): """Mock Supervisor.""" @@ -134,6 +150,19 @@ async def slow_server_version(*args): await asyncio.sleep(0.1) +@pytest.mark.parametrize( + "flow, flow_params", + [ + ( + "flow", + lambda entry: { + "handler": DOMAIN, + "context": {"source": config_entries.SOURCE_USER}, + }, + ), + ("options", lambda entry: {"handler": entry.entry_id}), + ], +) @pytest.mark.parametrize( "url, server_version_side_effect, server_version_timeout, error", [ @@ -157,20 +186,15 @@ async def slow_server_version(*args): ), ], ) -async def test_manual_errors( - hass, - url, - error, -): +async def test_manual_errors(hass, integration, url, error, flow, flow_params): """Test all errors with a manual set up.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + entry = integration + result = await getattr(hass.config_entries, flow).async_init(**flow_params(entry)) assert result["type"] == "form" assert result["step_id"] == "manual" - result = await hass.config_entries.flow.async_configure( + result = await getattr(hass.config_entries, flow).async_configure( result["flow_id"], { "url": url, @@ -1059,3 +1083,714 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ assert result["type"] == "abort" assert result["reason"] == "addon_install_failed" + + +async def test_options_manual(hass, client, integration): + """Test manual settings in options flow.""" + entry = integration + entry.unique_id = 1234 + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"url": "ws://1.1.1.1:3001"} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert entry.data["url"] == "ws://1.1.1.1:3001" + assert entry.data["use_addon"] is False + assert entry.data["integration_created_addon"] is False + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +async def test_options_manual_different_device(hass, integration): + """Test options flow manual step connecting to different device.""" + entry = integration + entry.unique_id = 5678 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"url": "ws://1.1.1.1:3001"} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "different_device" + + +async def test_options_not_addon(hass, client, supervisor, integration): + """Test options flow and opting out of add-on on Supervisor.""" + entry = integration + entry.unique_id = 1234 + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": False} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert entry.data["url"] == "ws://localhost:3000" + assert entry.data["use_addon"] is False + assert entry.data["integration_created_addon"] is False + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + {"device": "/test", "network_key": "abc123"}, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + {"use_addon": True}, + {"device": "/test", "network_key": "abc123"}, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 1, + ), + ], +) +async def test_options_addon_running( + hass, + client, + supervisor, + integration, + addon_running, + addon_options, + set_addon_options, + restart_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, + disconnect_calls, +): + """Test options flow and add-on already running on Supervisor.""" + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + + new_addon_options["device"] = new_addon_options.pop("usb_path") + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": new_addon_options}, + ) + assert client.disconnect.call_count == disconnect_calls + + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert restart_addon.call_args == call(hass, "core_zwave_js") + + assert result["type"] == "create_entry" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == new_addon_options["device"] + assert entry.data["network_key"] == new_addon_options["network_key"] + assert entry.data["use_addon"] is True + assert entry.data["integration_created_addon"] is False + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + { + "usb_path": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + ), + ], +) +async def test_options_addon_running_no_changes( + hass, + client, + supervisor, + integration, + addon_running, + addon_options, + set_addon_options, + restart_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, +): + """Test options flow without changes, and add-on already running on Supervisor.""" + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + await hass.async_block_till_done() + + new_addon_options["device"] = new_addon_options.pop("usb_path") + assert set_addon_options.call_count == 0 + assert restart_addon.call_count == 0 + + assert result["type"] == "create_entry" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == new_addon_options["device"] + assert entry.data["network_key"] == new_addon_options["network_key"] + assert entry.data["use_addon"] is True + assert entry.data["integration_created_addon"] is False + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +async def different_device_server_version(*args): + """Return server version for a device with different home id.""" + return VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=5678, + min_schema_version=0, + max_schema_version=1, + ) + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + different_device_server_version, + ), + ], +) +async def test_options_different_device( + hass, + client, + supervisor, + integration, + addon_running, + addon_options, + set_addon_options, + restart_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, + disconnect_calls, + server_version_side_effect, +): + """Test options flow and configuring a different device.""" + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + + assert set_addon_options.call_count == 1 + new_addon_options["device"] = new_addon_options.pop("usb_path") + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": new_addon_options}, + ) + assert client.disconnect.call_count == disconnect_calls + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + + assert restart_addon.call_count == 1 + assert restart_addon.call_args == call(hass, "core_zwave_js") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert set_addon_options.call_count == 2 + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": old_addon_options}, + ) + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + + assert restart_addon.call_count == 2 + assert restart_addon.call_args == call(hass, "core_zwave_js") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "different_device" + assert entry.data == data + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, restart_addon_side_effect", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + [HassioAPIError(), None], + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + [ + HassioAPIError(), + HassioAPIError(), + ], + ), + ], +) +async def test_options_addon_restart_failed( + hass, + client, + supervisor, + integration, + addon_running, + addon_options, + set_addon_options, + restart_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, + disconnect_calls, + restart_addon_side_effect, +): + """Test options flow and add-on restart failure.""" + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + + assert set_addon_options.call_count == 1 + new_addon_options["device"] = new_addon_options.pop("usb_path") + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": new_addon_options}, + ) + assert client.disconnect.call_count == disconnect_calls + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + + assert restart_addon.call_count == 1 + assert restart_addon.call_args == call(hass, "core_zwave_js") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert set_addon_options.call_count == 2 + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": old_addon_options}, + ) + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + + assert restart_addon.call_count == 2 + assert restart_addon.call_args == call(hass, "core_zwave_js") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "addon_start_failed" + assert entry.data == data + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, server_version_side_effect", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + { + "usb_path": "/test", + "network_key": "abc123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + aiohttp.ClientError("Boom"), + ), + ], +) +async def test_options_addon_running_server_info_failure( + hass, + client, + supervisor, + integration, + addon_running, + addon_options, + set_addon_options, + restart_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, + disconnect_calls, + server_version_side_effect, +): + """Test options flow and add-on already running with server info failure.""" + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + assert entry.data == data + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 + + +@pytest.mark.parametrize( + "discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + {"device": "/test", "network_key": "abc123"}, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + {"use_addon": True}, + {"device": "/test", "network_key": "abc123"}, + { + "usb_path": "/new", + "network_key": "new123", + "log_level": "info", + "emulate_hardware": False, + }, + 1, + ), + ], +) +async def test_options_addon_not_installed( + hass, + client, + supervisor, + addon_installed, + install_addon, + integration, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, + discovery_info, + entry_data, + old_addon_options, + new_addon_options, + disconnect_calls, +): + """Test options flow and add-on not installed on Supervisor.""" + addon_installed.return_value["version"] = None + addon_options.update(old_addon_options) + entry = integration + entry.unique_id = 1234 + data = {**entry.data, **entry_data} + hass.config_entries.async_update_entry(entry, data=data) + + assert entry.data["url"] == "ws://test.org" + + assert client.connect.call_count == 1 + assert client.disconnect.call_count == 0 + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "progress" + assert result["step_id"] == "install_addon" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + + assert install_addon.call_args == call(hass, "core_zwave_js") + + assert result["type"] == "form" + assert result["step_id"] == "configure_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + new_addon_options, + ) + + new_addon_options["device"] = new_addon_options.pop("usb_path") + assert set_addon_options.call_args == call( + hass, + "core_zwave_js", + {"options": new_addon_options}, + ) + assert client.disconnect.call_count == disconnect_calls + + assert result["type"] == "progress" + assert result["step_id"] == "start_addon" + + await hass.async_block_till_done() + + assert start_addon.call_count == 1 + assert start_addon.call_args == call(hass, "core_zwave_js") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == new_addon_options["device"] + assert entry.data["network_key"] == new_addon_options["network_key"] + assert entry.data["use_addon"] is True + assert entry.data["integration_created_addon"] is True + assert client.connect.call_count == 2 + assert client.disconnect.call_count == 1 From afa00b76264665fd2a307d2d25e70818d69a20f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 14:25:38 +0200 Subject: [PATCH 506/750] DSMR: Remove Gas derivative sensor (#52147) --- homeassistant/components/dsmr/sensor.py | 82 +------------------------ tests/components/dsmr/test_sensor.py | 47 +------------- 2 files changed, 4 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d023c71592b..cd6167d1487 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -15,12 +15,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, - TIME_HOURS, -) +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -172,7 +167,7 @@ async def async_setup_entry( else: gas_obis = obis_ref.GAS_METER_READING - # Add gas meter reading and derivative for usage + # Add gas meter reading devices += [ DSMREntity( "Gas Consumption", @@ -181,15 +176,7 @@ async def async_setup_entry( gas_obis, config, True, - ), - DerivativeDSMREntity( - "Hourly Gas Consumption", - DEVICE_NAME_GAS, - config[CONF_SERIAL_ID_GAS], - gas_obis, - config, - False, - ), + ) ] async_add_entities(devices) @@ -374,66 +361,3 @@ class DSMREntity(SensorEntity): return "low" return None - - -class DerivativeDSMREntity(DSMREntity): - """Calculated derivative for values where the DSMR doesn't offer one. - - Gas readings are only reported per hour and don't offer a rate only - the current meter reading. This entity converts subsequents readings - into a hourly rate. - """ - - _previous_reading = None - _previous_timestamp = None - _state = None - - @property - def state(self): - """Return the calculated current hourly rate.""" - return self._state - - @property - def force_update(self): - """Disable force update.""" - return False - - @property - def should_poll(self): - """Enable polling.""" - return True - - async def async_update(self): - """Recalculate hourly rate if timestamp has changed. - - DSMR updates gas meter reading every hour. Along with the new - value a timestamp is provided for the reading. Test if the last - known timestamp differs from the current one then calculate a - new rate for the previous hour. - - """ - # check if the timestamp for the object differs from the previous one - timestamp = self.get_dsmr_object_attr("datetime") - if timestamp and timestamp != self._previous_timestamp: - current_reading = self.get_dsmr_object_attr("value") - - if self._previous_reading is None: - # Can't calculate rate without previous datapoint - # just store current point - pass - else: - # Recalculate the rate - diff = current_reading - self._previous_reading - timediff = timestamp - self._previous_timestamp - total_seconds = timediff.total_seconds() - self._state = round(float(diff) / total_seconds * 3600, 3) - - self._previous_reading = current_reading - self._previous_timestamp = timestamp - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, per hour, if any.""" - unit = self.get_dsmr_object_attr("unit") - if unit: - return f"{unit}/{TIME_HOURS}" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 29ab29a0af6..973af27b373 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -13,13 +13,8 @@ from unittest.mock import DEFAULT, MagicMock from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN -from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ( - ENERGY_KILO_WATT_HOUR, - VOLUME_CUBIC_METERS, - VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, -) +from homeassistant.const import ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -179,46 +174,6 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): assert not entry -async def test_derivative(): - """Test calculation of derivative value.""" - from dsmr_parser.objects import MBusObject - - config = {"platform": "dsmr"} - - entity = DerivativeDSMREntity("test", "test_device", "5678", "1.0.0", config, False) - await entity.async_update() - - assert entity.state is None, "initial state not unknown" - - entity.telegram = { - "1.0.0": MBusObject( - [ - {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, - ] - ) - } - await entity.async_update() - - assert entity.state is None, "state after first update should still be unknown" - - entity.telegram = { - "1.0.0": MBusObject( - [ - {"value": datetime.datetime.fromtimestamp(1551642543)}, - {"value": Decimal(745.698), "unit": VOLUME_CUBIC_METERS}, - ] - ) - } - await entity.async_update() - - assert ( - abs(entity.state - 0.033) < 0.00001 - ), "state should be hourly usage calculated from first and second update" - - assert entity.unit_of_measurement == VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR - - async def test_v4_meter(hass, dsmr_connection_fixture): """Test if v4 meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From 09b3882a5b6193ebd289ecb08c2843b5c43fe5b0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 24 Jun 2021 16:01:28 +0200 Subject: [PATCH 507/750] Type frontend strictly (#52148) --- homeassistant/components/frontend/__init__.py | 118 +++++++++++------- homeassistant/components/frontend/storage.py | 32 +++-- 2 files changed, 95 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1b104982026..392806dc885 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,13 +1,14 @@ """Handle the frontend for Home Assistant.""" from __future__ import annotations +from collections.abc import Iterator from functools import lru_cache import json import logging import mimetypes import os import pathlib -from typing import Any +from typing import Any, TypedDict, cast from aiohttp import hdrs, web, web_urldispatcher import jinja2 @@ -16,18 +17,18 @@ from yarl import URL from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config import async_hass_config_yaml from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, bind_hass from .storage import async_setup_frontend_storage -# mypy: allow-untyped-defs, no-check-untyped-defs - # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/frontend/issues/3336 mimetypes.add_type("text/css", ".css") @@ -191,15 +192,15 @@ class UrlManager: on hass.data """ - def __init__(self, urls): + def __init__(self, urls: list[str]) -> None: """Init the url manager.""" self.urls = frozenset(urls) - def add(self, url): + def add(self, url: str) -> None: """Add a url to the set.""" self.urls = frozenset([*self.urls, url]) - def remove(self, url): + def remove(self, url: str) -> None: """Remove a url from the set.""" self.urls = self.urls - {url} @@ -208,7 +209,7 @@ class Panel: """Abstract class for panels.""" # Name of the webcomponent - component_name: str | None = None + component_name: str # Icon to show in the sidebar sidebar_icon: str | None = None @@ -227,13 +228,13 @@ class Panel: def __init__( self, - component_name, - sidebar_title, - sidebar_icon, - frontend_url_path, - config, - require_admin, - ): + component_name: str, + sidebar_title: str | None, + sidebar_icon: str | None, + frontend_url_path: str | None, + config: dict[str, Any] | None, + require_admin: bool, + ) -> None: """Initialize a built-in panel.""" self.component_name = component_name self.sidebar_title = sidebar_title @@ -243,7 +244,7 @@ class Panel: self.require_admin = require_admin @callback - def to_response(self): + def to_response(self) -> PanelRespons: """Panel as dictionary.""" return { "component_name": self.component_name, @@ -258,16 +259,16 @@ class Panel: @bind_hass @callback def async_register_built_in_panel( - hass, - component_name, - sidebar_title=None, - sidebar_icon=None, - frontend_url_path=None, - config=None, - require_admin=False, + hass: HomeAssistant, + component_name: str, + sidebar_title: str | None = None, + sidebar_icon: str | None = None, + frontend_url_path: str | None = None, + config: dict[str, Any] | None = None, + require_admin: bool = False, *, - update=False, -): + update: bool = False, +) -> None: """Register a built-in panel.""" panel = Panel( component_name, @@ -290,7 +291,7 @@ def async_register_built_in_panel( @bind_hass @callback -def async_remove_panel(hass, frontend_url_path): +def async_remove_panel(hass: HomeAssistant, frontend_url_path: str) -> None: """Remove a built-in panel.""" panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None) @@ -300,18 +301,18 @@ def async_remove_panel(hass, frontend_url_path): hass.bus.async_fire(EVENT_PANELS_UPDATED) -def add_extra_js_url(hass, url, es5=False): +def add_extra_js_url(hass: HomeAssistant, url: str, es5: bool = False) -> None: """Register extra js or module url to load.""" key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL hass.data[key].add(url) -def add_manifest_json_key(key, val): +def add_manifest_json_key(key: str, val: Any) -> None: """Add a keyval to the manifest.json.""" MANIFEST_JSON.update_key(key, val) -def _frontend_root(dev_repo_path): +def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: """Return root path to the frontend files.""" if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" @@ -319,17 +320,17 @@ def _frontend_root(dev_repo_path): # pylint: disable=import-outside-toplevel import hass_frontend - return hass_frontend.where() + return cast(pathlib.Path, hass_frontend.where()) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the serving of the frontend.""" await async_setup_frontend_storage(hass) hass.components.websocket_api.async_register_command(websocket_get_panels) hass.components.websocket_api.async_register_command(websocket_get_themes) hass.components.websocket_api.async_register_command(websocket_get_translations) hass.components.websocket_api.async_register_command(websocket_get_version) - hass.http.register_view(ManifestJSONView) + hass.http.register_view(ManifestJSONView()) conf = config.get(DOMAIN, {}) @@ -396,7 +397,9 @@ async def async_setup(hass, config): return True -async def _async_setup_themes(hass, themes): +async def _async_setup_themes( + hass: HomeAssistant, themes: dict[str, Any] | None +) -> None: """Set up themes data and services.""" hass.data[DATA_THEMES] = themes or {} @@ -417,7 +420,7 @@ async def _async_setup_themes(hass, themes): hass.data[DATA_DEFAULT_DARK_THEME] = dark_theme_name @callback - def update_theme_and_fire_event(): + def update_theme_and_fire_event() -> None: """Update theme_color in manifest.""" name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] @@ -434,7 +437,7 @@ async def _async_setup_themes(hass, themes): hass.bus.async_fire(EVENT_THEMES_UPDATED) @callback - def set_theme(call): + def set_theme(call: ServiceCall) -> None: """Set backend-preferred theme.""" name = call.data[CONF_NAME] mode = call.data.get("mode", "light") @@ -466,7 +469,7 @@ async def _async_setup_themes(hass, themes): ) update_theme_and_fire_event() - async def reload_themes(_): + async def reload_themes(_: ServiceCall) -> None: """Reload themes.""" config = await async_hass_config_yaml(hass) new_themes = config[DOMAIN].get(CONF_THEMES, {}) @@ -500,19 +503,19 @@ async def _async_setup_themes(hass, themes): @callback @lru_cache(maxsize=1) -def _async_render_index_cached(template, **kwargs): +def _async_render_index_cached(template: jinja2.Template, **kwargs: Any) -> str: return template.render(**kwargs) class IndexView(web_urldispatcher.AbstractResource): """Serve the frontend.""" - def __init__(self, repo_path, hass): + def __init__(self, repo_path: str | None, hass: HomeAssistant) -> None: """Initialize the frontend view.""" super().__init__(name="frontend:index") self.repo_path = repo_path self.hass = hass - self._template_cache = None + self._template_cache: jinja2.Template | None = None @property def canonical(self) -> str: @@ -520,7 +523,7 @@ class IndexView(web_urldispatcher.AbstractResource): return "/" @property - def _route(self): + def _route(self) -> web_urldispatcher.ResourceRoute: """Return the index route.""" return web_urldispatcher.ResourceRoute("GET", self.get, self) @@ -552,7 +555,7 @@ class IndexView(web_urldispatcher.AbstractResource): Required for subapplications support. """ - def get_info(self): + def get_info(self) -> dict[str, list[str]]: # type: ignore[override] """Return a dict with additional info useful for introspection.""" return {"panels": list(self.hass.data[DATA_PANELS])} @@ -562,7 +565,7 @@ class IndexView(web_urldispatcher.AbstractResource): def raw_match(self, path: str) -> bool: """Perform a raw match against path.""" - def get_template(self): + def get_template(self) -> jinja2.Template: """Get template.""" tpl = self._template_cache if tpl is None: @@ -600,7 +603,7 @@ class IndexView(web_urldispatcher.AbstractResource): """Return length of resource.""" return 1 - def __iter__(self): + def __iter__(self) -> Iterator[web_urldispatcher.ResourceRoute]: """Iterate over routes.""" return iter([self._route]) @@ -613,7 +616,7 @@ class ManifestJSONView(HomeAssistantView): name = "manifestjson" @callback - def get(self, request): # pylint: disable=no-self-use + def get(self, request: web.Request) -> web.Response: # pylint: disable=no-self-use """Return the manifest.json.""" return web.Response( text=MANIFEST_JSON.json, content_type="application/manifest+json" @@ -622,7 +625,9 @@ class ManifestJSONView(HomeAssistantView): @callback @websocket_api.websocket_command({"type": "get_panels"}) -def websocket_get_panels(hass, connection, msg): +def websocket_get_panels( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get panels command.""" user_is_admin = connection.user.is_admin panels = { @@ -636,7 +641,9 @@ def websocket_get_panels(hass, connection, msg): @callback @websocket_api.websocket_command({"type": "frontend/get_themes"}) -def websocket_get_themes(hass, connection, msg): +def websocket_get_themes( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get themes command.""" if hass.config.safe_mode: connection.send_message( @@ -677,7 +684,9 @@ def websocket_get_themes(hass, connection, msg): } ) @websocket_api.async_response -async def websocket_get_translations(hass, connection, msg): +async def websocket_get_translations( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get translations command.""" resources = await async_get_translations( hass, @@ -693,7 +702,9 @@ async def websocket_get_translations(hass, connection, msg): @websocket_api.websocket_command({"type": "frontend/get_version"}) @websocket_api.async_response -async def websocket_get_version(hass, connection, msg): +async def websocket_get_version( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Handle get version command.""" integration = await async_get_integration(hass, "frontend") @@ -707,3 +718,14 @@ async def websocket_get_version(hass, connection, msg): connection.send_error(msg["id"], "unknown_version", "Version not found") else: connection.send_result(msg["id"], {"version": frontend}) + + +class PanelRespons(TypedDict): + """Represent the panel response type.""" + + component_name: str + icon: str | None + title: str | None + config: dict[str, Any] | None + url_path: str | None + require_admin: bool diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index b37945b5e07..294b707c965 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -1,28 +1,34 @@ """API for persistent storage for the frontend.""" +from __future__ import annotations + from functools import wraps +from typing import Any, Callable import voluptuous as vol from homeassistant.components import websocket_api - -# mypy: allow-untyped-calls, allow-untyped-defs +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant +from homeassistant.helpers.storage import Store DATA_STORAGE = "frontend_storage" STORAGE_VERSION_USER_DATA = 1 -async def async_setup_frontend_storage(hass): +async def async_setup_frontend_storage(hass: HomeAssistant) -> None: """Set up frontend storage.""" hass.data[DATA_STORAGE] = ({}, {}) hass.components.websocket_api.async_register_command(websocket_set_user_data) hass.components.websocket_api.async_register_command(websocket_get_user_data) -def with_store(orig_func): +def with_store(orig_func: Callable) -> Callable: """Decorate function to provide data.""" @wraps(orig_func) - async def with_store_func(hass, connection, msg): + async def with_store_func( + hass: HomeAssistant, connection: ActiveConnection, msg: dict + ) -> None: """Provide user specific data and store to function.""" stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id @@ -50,7 +56,13 @@ def with_store(orig_func): ) @websocket_api.async_response @with_store -async def websocket_set_user_data(hass, connection, msg, store, data): +async def websocket_set_user_data( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + store: Store, + data: dict[str, Any], +) -> None: """Handle set global data command. Async friendly. @@ -65,7 +77,13 @@ async def websocket_set_user_data(hass, connection, msg, store, data): ) @websocket_api.async_response @with_store -async def websocket_get_user_data(hass, connection, msg, store, data): +async def websocket_get_user_data( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + store: Store, + data: dict[str, Any], +) -> None: """Handle get global data command. Async friendly. From 04c9665241ce3f19f65c6d711a7dd802ac4f38a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Jun 2021 16:22:54 +0200 Subject: [PATCH 508/750] Filter MQTT JSON attributes (#52076) * Filter JSON attributes * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Refactor, add tests Co-authored-by: Paulus Schoutsen --- homeassistant/components/mqtt/mixins.py | 30 +++++++++++++++++++++-- tests/components/mqtt/test_common.py | 32 +++++++++++++++++++++++++ tests/components/mqtt/test_sensor.py | 8 +++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9b1c7a9fb21..46c4a88e8fe 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -67,6 +67,25 @@ CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" CONF_SUGGESTED_AREA = "suggested_area" +MQTT_ATTRIBUTES_BLOCKED = { + "assumed_state", + "available", + "context_recent_time", + "device_class", + "device_info", + "entity_picture", + "entity_registry_enabled_default", + "extra_state_attributes", + "force_update", + "icon", + "name", + "should_poll", + "state", + "supported_features", + "unique_id", + "unit_of_measurement", +} + MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( { vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, @@ -175,11 +194,12 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): class MqttAttributes(Entity): """Mixin used for platforms that support JSON attributes.""" - def __init__(self, config: dict) -> None: + def __init__(self, config: dict, extra_blocked_attributes: list = None) -> None: """Initialize the JSON attributes mixin.""" self._attributes = None self._attributes_sub_state = None self._attributes_config = config + self._extra_blocked_attributes = extra_blocked_attributes or [] async def async_added_to_hass(self) -> None: """Subscribe MQTT events.""" @@ -206,7 +226,13 @@ class MqttAttributes(Entity): payload = attr_tpl.async_render_with_possible_json_value(payload) json_dict = json.loads(payload) if isinstance(json_dict, dict): - self._attributes = json_dict + filtered_dict = { + k: v + for k, v in json_dict.items() + if k not in MQTT_ATTRIBUTES_BLOCKED + and k not in self._extra_blocked_attributes + } + self._attributes = filtered_dict self.async_write_ha_state() else: _LOGGER.warning("JSON result was not a dictionary") diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index fc8d26843d0..d8a263131a9 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -7,6 +7,7 @@ from unittest.mock import ANY, patch from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.const import MQTT_DISCONNECTED +from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -493,6 +494,37 @@ async def help_test_setting_attribute_via_mqtt_json_message( assert state.attributes.get("val") == "100" +async def help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, domain, config, extra_blocked_attributes +): + """Test the setting of blocked attribute via MQTT with JSON payload. + + This is a test helper for the MqttAttributes mixin. + """ + extra_blocked_attributes = extra_blocked_attributes or [] + + # Add JSON attributes settings to config + config = copy.deepcopy(config) + config[domain]["json_attributes_topic"] = "attr-topic" + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + val = "abc123" + + for attr in MQTT_ATTRIBUTES_BLOCKED: + async_fire_mqtt_message(hass, "attr-topic", json.dumps({attr: val})) + state = hass.states.get(f"{domain}.test") + assert state.attributes.get(attr) != val + + for attr in extra_blocked_attributes: + async_fire_mqtt_message(hass, "attr-topic", json.dumps({attr: val})) + state = hass.states.get(f"{domain}.test") + assert state.attributes.get(attr) != val + + async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config): """Test the setting of attribute via MQTT with JSON payload. diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7d732849906..d90737a7b7f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -42,6 +42,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -531,6 +532,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 75c3daa45f205eb9414edb5489e916f88626ee51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 16:56:43 +0200 Subject: [PATCH 509/750] DSMR: Refactor sensor creation, added typing to sensors (#52153) * DSMR: Refactor sensor creation, added typing to sensors * Log from package level * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/dsmr/config_flow.py | 6 +- homeassistant/components/dsmr/const.py | 166 +++++++++++++++ homeassistant/components/dsmr/models.py | 16 ++ homeassistant/components/dsmr/sensor.py | 212 ++++++------------- 4 files changed, 249 insertions(+), 151 deletions(-) create mode 100644 homeassistant/components/dsmr/models.py diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 1ddbdb480c1..294526a386e 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from functools import partial -import logging import os from typing import Any @@ -29,10 +28,9 @@ from .const import ( CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, + LOGGER, ) -_LOGGER = logging.getLogger(__name__) - CONF_MANUAL_PATH = "Enter Manually" @@ -92,7 +90,7 @@ class DSMRConnection: try: transport, protocol = await asyncio.create_task(reader_factory()) except (serial.serialutil.SerialException, OSError): - _LOGGER.exception("Error connecting to DSMR") + LOGGER.exception("Error connecting to DSMR") return False if transport: diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index da804857845..44d3453146b 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -1,7 +1,16 @@ """Constants for the DSMR integration.""" +from __future__ import annotations + +import logging + +from dsmr_parser import obis_references + +from .models import DSMRSensor DOMAIN = "dsmr" +LOGGER = logging.getLogger(__package__) + PLATFORMS = ["sensor"] CONF_DSMR_VERSION = "dsmr_version" @@ -28,3 +37,160 @@ ICON_GAS = "mdi:fire" ICON_POWER = "mdi:flash" ICON_POWER_FAILURE = "mdi:flash-off" ICON_SWELL_SAG = "mdi:pulse" + + +SENSORS: list[DSMRSensor] = [ + DSMRSensor( + name="Power Consumption", + obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, + force_update=True, + ), + DSMRSensor( + name="Power Production", + obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, + force_update=True, + ), + DSMRSensor( + name="Power Tariff", + obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, + ), + DSMRSensor( + name="Energy Consumption (tarif 1)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, + force_update=True, + ), + DSMRSensor( + name="Energy Consumption (tarif 2)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, + force_update=True, + ), + DSMRSensor( + name="Energy Production (tarif 1)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + force_update=True, + ), + DSMRSensor( + name="Energy Production (tarif 2)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + force_update=True, + ), + DSMRSensor( + name="Power Consumption Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + ), + DSMRSensor( + name="Power Consumption Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + ), + DSMRSensor( + name="Power Consumption Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + ), + DSMRSensor( + name="Power Production Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + ), + DSMRSensor( + name="Power Production Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + ), + DSMRSensor( + name="Power Production Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + ), + DSMRSensor( + name="Short Power Failure Count", + obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, + ), + DSMRSensor( + name="Long Power Failure Count", + obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, + ), + DSMRSensor( + name="Voltage Sags Phase L1", + obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, + ), + DSMRSensor( + name="Voltage Sags Phase L2", + obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, + ), + DSMRSensor( + name="Voltage Sags Phase L3", + obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, + ), + DSMRSensor( + name="Voltage Swells Phase L1", + obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, + ), + DSMRSensor( + name="Voltage Swells Phase L2", + obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, + ), + DSMRSensor( + name="Voltage Swells Phase L3", + obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, + ), + DSMRSensor( + name="Voltage Phase L1", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, + ), + DSMRSensor( + name="Voltage Phase L2", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, + ), + DSMRSensor( + name="Voltage Phase L3", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, + ), + DSMRSensor( + name="Current Phase L1", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, + ), + DSMRSensor( + name="Current Phase L2", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, + ), + DSMRSensor( + name="Current Phase L3", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, + ), + DSMRSensor( + name="Energy Consumption (total)", + obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + dsmr_versions={"5L"}, + force_update=True, + ), + DSMRSensor( + name="Energy Production (total)", + obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + dsmr_versions={"5L"}, + force_update=True, + ), + DSMRSensor( + name="Energy Consumption (total)", + obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, + dsmr_versions={"2.2", "4", "5", "5B"}, + force_update=True, + ), + DSMRSensor( + name="Gas Consumption", + obis_reference=obis_references.HOURLY_GAS_METER_READING, + dsmr_versions={"4", "5", "5L"}, + force_update=True, + is_gas=True, + ), + DSMRSensor( + name="Gas Consumption", + obis_reference=obis_references.BELGIUM_HOURLY_GAS_METER_READING, + dsmr_versions={"5B"}, + force_update=True, + is_gas=True, + ), + DSMRSensor( + name="Gas Consumption", + obis_reference=obis_references.GAS_METER_READING, + dsmr_versions={"2.2"}, + force_update=True, + is_gas=True, + ), +] diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py new file mode 100644 index 00000000000..a6568555e11 --- /dev/null +++ b/homeassistant/components/dsmr/models.py @@ -0,0 +1,16 @@ +"""Models for the DSMR integration.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class DSMRSensor: + """Represents an DSMR Sensor.""" + + name: str + obis_reference: str + + dsmr_versions: set[str] | None = None + force_update: bool = False + is_gas: bool = False diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index cd6167d1487..32d0c381164 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -6,10 +6,11 @@ from asyncio import CancelledError from contextlib import suppress from datetime import timedelta from functools import partial -import logging +from typing import Any from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.objects import DSMRObject import serial import voluptuous as vol @@ -18,6 +19,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.util import Throttle from .const import ( @@ -40,9 +43,10 @@ from .const import ( ICON_POWER, ICON_POWER_FAILURE, ICON_SWELL_SAG, + LOGGER, + SENSORS, ) - -_LOGGER = logging.getLogger(__name__) +from .models import DSMRSensor PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -57,7 +61,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Import the platform into a config entry.""" hass.async_create_task( hass.config_entries.flow.async_init( @@ -67,139 +76,37 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the DSMR sensor.""" - config = entry.data - options = entry.options - - dsmr_version = config[CONF_DSMR_VERSION] - - # Define list of name,obis,force_update mappings to generate entities - obis_mapping = [ - ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE, True], - ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY, True], - ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF, False], - ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1, True], - ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2, True], - ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1, True], - ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2, True], - [ - "Power Consumption Phase L1", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, - False, - ], - [ - "Power Consumption Phase L2", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, - False, - ], - [ - "Power Consumption Phase L3", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, - False, - ], - [ - "Power Production Phase L1", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, - False, - ], - [ - "Power Production Phase L2", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, - False, - ], - [ - "Power Production Phase L3", - obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, - False, - ], - ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT, False], - ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT, False], - ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT, False], - ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT, False], - ["Voltage Sags Phase L3", obis_ref.VOLTAGE_SAG_L3_COUNT, False], - ["Voltage Swells Phase L1", obis_ref.VOLTAGE_SWELL_L1_COUNT, False], - ["Voltage Swells Phase L2", obis_ref.VOLTAGE_SWELL_L2_COUNT, False], - ["Voltage Swells Phase L3", obis_ref.VOLTAGE_SWELL_L3_COUNT, False], - ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1, False], - ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2, False], - ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3, False], - ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1, False], - ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2, False], - ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3, False], + dsmr_version = entry.data[CONF_DSMR_VERSION] + entities = [ + DSMREntity(sensor, entry) + for sensor in SENSORS + if (sensor.dsmr_versions is None or dsmr_version in sensor.dsmr_versions) + and (not sensor.is_gas or CONF_SERIAL_ID_GAS in entry.data) ] - - if dsmr_version == "5L": - obis_mapping.extend( - [ - [ - "Energy Consumption (total)", - obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, - True, - ], - [ - "Energy Production (total)", - obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, - True, - ], - ] - ) - else: - obis_mapping.extend( - [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL, True]] - ) - - # Generate device entities - devices = [ - DSMREntity( - name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config, force_update - ) - for name, obis, force_update in obis_mapping - ] - - # Protocol version specific obis - if CONF_SERIAL_ID_GAS in config: - if dsmr_version in ("4", "5", "5L"): - gas_obis = obis_ref.HOURLY_GAS_METER_READING - elif dsmr_version in ("5B",): - gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING - else: - gas_obis = obis_ref.GAS_METER_READING - - # Add gas meter reading - devices += [ - DSMREntity( - "Gas Consumption", - DEVICE_NAME_GAS, - config[CONF_SERIAL_ID_GAS], - gas_obis, - config, - True, - ) - ] - - async_add_entities(devices) + async_add_entities(entities) min_time_between_updates = timedelta( - seconds=options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE) + seconds=entry.options.get(CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE) ) @Throttle(min_time_between_updates) - def update_entities_telegram(telegram): + def update_entities_telegram(telegram: dict[str, DSMRObject]): """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram - for device in devices: - device.update_data(telegram) + for entity in entities: + entity.update_data(telegram) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival - if CONF_HOST in config: + if CONF_HOST in entry.data: reader_factory = partial( create_tcp_dsmr_reader, - config[CONF_HOST], - config[CONF_PORT], - config[CONF_DSMR_VERSION], + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop, keep_alive_interval=60, @@ -207,13 +114,13 @@ async def async_setup_entry( else: reader_factory = partial( create_dsmr_reader, - config[CONF_PORT], - config[CONF_DSMR_VERSION], + entry.data[CONF_PORT], + entry.data[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop, ) - async def connect_and_reconnect(): + async def connect_and_reconnect() -> None: """Connect to DSMR and keep reconnecting until Home Assistant stops.""" stop_listener = None transport = None @@ -245,12 +152,12 @@ async def async_setup_entry( update_entities_telegram({}) # throttle reconnect attempts - await asyncio.sleep(config[CONF_RECONNECT_INTERVAL]) + await asyncio.sleep(entry.data[CONF_RECONNECT_INTERVAL]) except (serial.serialutil.SerialException, OSError): # Log any error while establishing connection and drop to retry # connection wait - _LOGGER.exception("Error connecting to DSMR") + LOGGER.exception("Error connecting to DSMR") transport = None protocol = None except CancelledError: @@ -277,40 +184,48 @@ class DSMREntity(SensorEntity): _attr_should_poll = False - def __init__(self, name, device_name, device_serial, obis, config, force_update): + def __init__(self, sensor: DSMRSensor, entry: ConfigEntry) -> None: """Initialize entity.""" - self._obis = obis - self._config = config - self.telegram = {} + self._sensor = sensor + self._entry = entry + self.telegram: dict[str, DSMRObject] = {} - self._attr_name = name - self._attr_force_update = force_update - self._attr_unique_id = f"{device_serial}_{name}".replace(" ", "_") + device_serial = entry.data[CONF_SERIAL_ID] + device_name = DEVICE_NAME_ENERGY + if sensor.is_gas: + device_serial = entry.data[CONF_SERIAL_ID_GAS] + device_name = DEVICE_NAME_GAS + + self._attr_name = sensor.name + self._attr_force_update = sensor.force_update + self._attr_unique_id = f"{device_serial}_{sensor.name}".replace(" ", "_") self._attr_device_info = { "identifiers": {(DOMAIN, device_serial)}, "name": device_name, } @callback - def update_data(self, telegram): + def update_data(self, telegram: dict[str, DSMRObject]) -> None: """Update data.""" self.telegram = telegram - if self.hass and self._obis in self.telegram: + if self.hass and self._sensor.obis_reference in self.telegram: self.async_write_ha_state() - def get_dsmr_object_attr(self, attribute): + def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self._obis not in self.telegram: + if self._sensor.obis_reference not in self.telegram: return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self._obis] + dsmr_object = self.telegram[self._sensor.obis_reference] return getattr(dsmr_object, attribute, None) @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" + if not self.name: + return None if "Sags" in self.name or "Swells" in self.name: return ICON_SWELL_SAG if "Failure" in self.name: @@ -319,18 +234,21 @@ class DSMREntity(SensorEntity): return ICON_POWER if "Gas" in self.name: return ICON_GAS + return None @property - def state(self): + def state(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" value = self.get_dsmr_object_attr("value") + if value is None: + return None - if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: - return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) + if self._sensor.obis_reference == obis_ref.ELECTRICITY_ACTIVE_TARIFF: + return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): value = round( - float(value), self._config.get(CONF_PRECISION, DEFAULT_PRECISION) + float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION) ) if value is not None: @@ -339,16 +257,16 @@ class DSMREntity(SensorEntity): return None @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.get_dsmr_object_attr("unit") @staticmethod - def translate_tariff(value, dsmr_version): + def translate_tariff(value: str, dsmr_version: str) -> str | None: """Convert 2/1 to normal/low depending on DSMR version.""" # DSMR V5B: Note: In Belgium values are swapped: # Rate code 2 is used for low rate and rate code 1 is used for normal rate. - if dsmr_version in ("5B",): + if dsmr_version == "5B": if value == "0001": value = "0002" elif value == "0002": From 3b8ece38b395c9e7118e3cef52509749f0821fcb Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 24 Jun 2021 17:02:41 +0200 Subject: [PATCH 510/750] Second part of Strict types for Fritz (#52086) Co-authored-by: Shay Levy Co-authored-by: Martin Hjelmare --- .../components/fritz/binary_sensor.py | 11 +- homeassistant/components/fritz/common.py | 140 ++++++++++++------ homeassistant/components/fritz/config_flow.py | 74 +++++---- .../components/fritz/device_tracker.py | 7 +- homeassistant/components/fritz/sensor.py | 5 +- 5 files changed, 155 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index 2154a397584..7655df6e298 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -24,7 +24,7 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box binary sensors") fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] - if "WANIPConn1" in fritzbox_tools.connection.services: + if fritzbox_tools.connection and "WANIPConn1" in fritzbox_tools.connection.services: # Only routers are supported at the moment async_add_entities( [FritzBoxConnectivitySensor(fritzbox_tools, entry.title)], True @@ -74,14 +74,19 @@ class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): _LOGGER.debug("Updating FRITZ!Box binary sensors") self._is_on = True try: - if "WANCommonInterfaceConfig1" in self._fritzbox_tools.connection.services: + if ( + self._fritzbox_tools.connection + and "WANCommonInterfaceConfig1" + in self._fritzbox_tools.connection.services + ): link_props = self._fritzbox_tools.connection.call_action( "WANCommonInterfaceConfig1", "GetCommonLinkProperties" ) is_up = link_props["NewPhysicalLinkStatus"] self._is_on = is_up == "Up" else: - self._is_on = self._fritzbox_tools.fritz_status.is_connected + if self._fritzbox_tools.fritz_status: + self._is_on = self._fritzbox_tools.fritz_status.is_connected self._is_available = True diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 84288fe7fb3..a255ef6439f 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -1,12 +1,12 @@ """Support for AVM FRITZ!Box classes.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime, timedelta import logging -from typing import Any +from types import MappingProxyType +from typing import Any, TypedDict -# pylint: disable=import-error from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( FritzActionError, @@ -20,10 +20,11 @@ from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -40,6 +41,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +class ClassSetupMissing(Exception): + """Raised when a Class func is called before setup.""" + + def __init__(self): + """Init custom exception.""" + super().__init__("Function called before Class setup") + + @dataclass class Device: """FRITZ!Box device class.""" @@ -49,39 +58,48 @@ class Device: name: str +class HostInfo(TypedDict): + """FRITZ!Box host info class.""" + + mac: str + name: str + ip: str + status: bool + + class FritzBoxTools: """FrtizBoxTools class.""" def __init__( self, - hass, - password, - username=DEFAULT_USERNAME, - host=DEFAULT_HOST, - port=DEFAULT_PORT, - ): + hass: HomeAssistant, + password: str, + username: str = DEFAULT_USERNAME, + host: str = DEFAULT_HOST, + port: int = DEFAULT_PORT, + ) -> None: """Initialize FritzboxTools class.""" - self._cancel_scan = None + self._cancel_scan: CALLBACK_TYPE | None = None self._devices: dict[str, Any] = {} - self._options = None - self._unique_id = None - self.connection = None - self.fritz_hosts = None - self.fritz_status = None + self._options: MappingProxyType[str, Any] | None = None + self._unique_id: str | None = None + self.connection: FritzConnection = None + self.fritz_hosts: FritzHosts = None + self.fritz_status: FritzStatus = None self.hass = hass self.host = host self.password = password self.port = port self.username = username - self.mac = None - self.model = None - self.sw_version = None + self._mac: str | None = None + self._model: str | None = None + self._sw_version: str | None = None - async def async_setup(self): + async def async_setup(self) -> None: """Wrap up FritzboxTools class setup.""" - return await self.hass.async_add_executor_job(self.setup) + await self.hass.async_add_executor_job(self.setup) - def setup(self): + def setup(self) -> None: """Set up FritzboxTools class.""" self.connection = FritzConnection( address=self.host, @@ -93,14 +111,13 @@ class FritzBoxTools: self.fritz_status = FritzStatus(fc=self.connection) info = self.connection.call_action("DeviceInfo:1", "GetInfo") - if self._unique_id is None: + if not self._unique_id: self._unique_id = info["NewSerialNumber"] - self.model = info.get("NewModelName") - self.sw_version = info.get("NewSoftwareVersion") - self.mac = self.unique_id + self._model = info.get("NewModelName") + self._sw_version = info.get("NewSoftwareVersion") - async def async_start(self, options): + async def async_start(self, options: MappingProxyType[str, Any]) -> None: """Start FritzHosts connection.""" self.fritz_hosts = FritzHosts(fc=self.connection) self._options = options @@ -111,7 +128,7 @@ class FritzBoxTools: ) @callback - def async_unload(self): + def async_unload(self) -> None: """Unload FritzboxTools class.""" _LOGGER.debug("Unloading FRITZ!Box router integration") if self._cancel_scan is not None: @@ -119,8 +136,31 @@ class FritzBoxTools: self._cancel_scan = None @property - def unique_id(self): + def unique_id(self) -> str: """Return unique id.""" + if not self._unique_id: + raise ClassSetupMissing() + return self._unique_id + + @property + def model(self) -> str: + """Return device model.""" + if not self._model: + raise ClassSetupMissing() + return self._model + + @property + def sw_version(self) -> str: + """Return SW version.""" + if not self._sw_version: + raise ClassSetupMissing() + return self._sw_version + + @property + def mac(self) -> str: + """Return device Mac address.""" + if not self._unique_id: + raise ClassSetupMissing() return self._unique_id @property @@ -138,7 +178,7 @@ class FritzBoxTools: """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - def _update_info(self): + def _update_info(self) -> list[HostInfo]: """Retrieve latest information from the FRITZ!Box.""" return self.fritz_hosts.get_hosts_info() @@ -146,9 +186,12 @@ class FritzBoxTools: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) - consider_home = self._options.get( - CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() - ) + if self._options: + consider_home = self._options.get( + CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() + ) + else: + consider_home = DEFAULT_CONSIDER_HOME new_device = False for known_host in self._update_info(): @@ -165,7 +208,7 @@ class FritzBoxTools: if dev_mac in self._devices: self._devices[dev_mac].update(dev_info, dev_home, consider_home) else: - device = FritzDevice(dev_mac) + device = FritzDevice(dev_mac, dev_name) device.update(dev_info, dev_home, consider_home) self._devices[dev_mac] = device new_device = True @@ -177,6 +220,10 @@ class FritzBoxTools: async def service_fritzbox(self, service: str) -> None: """Define FRITZ!Box services.""" _LOGGER.debug("FRITZ!Box router: %s", service) + + if not self.connection: + raise HomeAssistantError("Unable to establish a connection") + try: if service == SERVICE_REBOOT: await self.hass.async_add_executor_job( @@ -194,26 +241,25 @@ class FritzBoxTools: raise HomeAssistantError("Service not supported") from ex +@dataclass class FritzData: """Storage class for platform global data.""" - def __init__(self) -> None: - """Initialize the data.""" - self.tracked: dict = {} + tracked: dict = field(default_factory=dict) class FritzDevice: """FritzScanner device.""" - def __init__(self, mac, name=None): + def __init__(self, mac: str, name: str) -> None: """Initialize device info.""" self._mac = mac self._name = name - self._ip_address = None - self._last_activity = None + self._ip_address: str | None = None + self._last_activity: datetime | None = None self._connected = False - def update(self, dev_info, dev_home, consider_home): + def update(self, dev_info: Device, dev_home: bool, consider_home: float) -> None: """Update device info.""" utc_point_in_time = dt_util.utcnow() @@ -235,27 +281,27 @@ class FritzDevice: self._ip_address = dev_info.ip_address if self._connected else None @property - def is_connected(self): + def is_connected(self) -> bool: """Return connected status.""" return self._connected @property - def mac_address(self): + def mac_address(self) -> str: """Get MAC address.""" return self._mac @property - def hostname(self): + def hostname(self) -> str: """Get Name.""" return self._name @property - def ip_address(self): + def ip_address(self) -> str | None: """Get IP address.""" return self._ip_address @property - def last_activity(self): + def last_activity(self) -> datetime | None: """Return device last activity.""" return self._last_activity @@ -274,7 +320,7 @@ class FritzBoxBaseEntity: return self._fritzbox_tools.mac @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information.""" return { diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 4001dcadc71..5ca351cdec1 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from typing import Any -from urllib.parse import urlparse +from urllib.parse import ParseResult, urlparse from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import voluptuous as vol @@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .common import FritzBoxTools from .const import ( @@ -42,23 +43,26 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return FritzBoxToolsOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize FRITZ!Box Tools flow.""" - self._host = None - self._entry = None - self._name = None - self._password = None - self._port = None - self._username = None - self.import_schema = None - self.fritz_tools = None + self._host: str | None = None + self._entry: ConfigEntry + self._name: str + self._password: str + self._port: int | None = None + self._username: str + self.fritz_tools: FritzBoxTools - async def fritz_tools_init(self): + async def fritz_tools_init(self) -> str | None: """Initialize FRITZ!Box Tools class.""" + + if not self._host or not self._port: + return None + self.fritz_tools = FritzBoxTools( hass=self.hass, host=self._host, @@ -87,7 +91,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return None @callback - def _async_create_entry(self): + def _async_create_entry(self) -> FlowResult: """Async create flow handler entry.""" return self.async_create_entry( title=self._name, @@ -102,12 +106,14 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by discovery.""" - ssdp_location = urlparse(discovery_info[ATTR_SSDP_LOCATION]) + ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION]) self._host = ssdp_location.hostname self._port = ssdp_location.port - self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) + self._name = ( + discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) or self.fritz_tools.model + ) self.context[CONF_HOST] = self._host if uuid := discovery_info.get(ATTR_UPNP_UDN): @@ -130,7 +136,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): } return await self.async_step_confirm() - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is None: return self._show_setup_form_confirm() @@ -148,7 +156,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - def _show_setup_form_init(self, errors=None): + def _show_setup_form_init(self, errors: dict[str, str] | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -163,7 +171,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - def _show_setup_form_confirm(self, errors=None): + def _show_setup_form_confirm( + self, errors: dict[str, str] | None = None + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="confirm", @@ -177,7 +187,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form_init() @@ -197,24 +209,28 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_reauth(self, data): + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle flow upon an API authentication error.""" - self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if cfg_entry := self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ): + self._entry = cfg_entry self._host = data[CONF_HOST] self._port = data[CONF_PORT] self._username = data[CONF_USERNAME] self._password = data[CONF_PASSWORD] return await self.async_step_reauth_confirm() - def _show_setup_form_reauth_confirm(self, user_input, errors=None): + def _show_setup_form_reauth_confirm( + self, user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> FlowResult: """Show the reauth form to the user.""" + default_username = user_input.get(CONF_USERNAME) return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( { - vol.Required( - CONF_USERNAME, default=user_input.get(CONF_USERNAME) - ): str, + vol.Required(CONF_USERNAME, default=default_username): str, vol.Required(CONF_PASSWORD): str, } ), @@ -222,7 +238,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self._show_setup_form_reauth_confirm( @@ -249,7 +267,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_abort(reason="reauth_successful") - async def async_step_import(self, import_config): + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user( { diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 0e75a781c5d..d4ff1dbd161 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,6 +1,7 @@ """Support for FRITZ!Box routers.""" from __future__ import annotations +import datetime import logging import voluptuous as vol @@ -120,9 +121,9 @@ class FritzBoxTracker(ScannerEntity): def __init__(self, router: FritzBoxTools, device: FritzDevice) -> None: """Initialize a FRITZ!Box device.""" self._router = router - self._mac = device.mac_address - self._name = device.hostname or DEFAULT_DEVICE_NAME - self._last_activity = device.last_activity + self._mac: str = device.mac_address + self._name: str = device.hostname or DEFAULT_DEVICE_NAME + self._last_activity: datetime.datetime | None = device.last_activity self._active = False @property diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 7bff6bd40c8..6d3a8f33c3c 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -74,7 +74,10 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box sensors") fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] - if "WANIPConn1" not in fritzbox_tools.connection.services: + if ( + not fritzbox_tools.connection + or "WANIPConn1" not in fritzbox_tools.connection.services + ): # Only routers are supported at the moment return From 69a04cf748bc6dd8b37d5dd1ecaa6471caed72a3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 24 Jun 2021 17:03:19 +0200 Subject: [PATCH 511/750] Fix Xiaomi Miio missing gateway info (#52146) During my last PR: https://github.com/home-assistant/core/pull/47955, I accedently created a bug that will block the setup of the gateway integration. This fixes that bug. --- homeassistant/components/xiaomi_miio/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 7482dae5d77..7dd84cf7812 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -74,7 +74,7 @@ class ConnectXiaomiGateway: try: self._gateway_device = gateway.Gateway(self._host, self._token) # get the gateway info - self._gateway_device.info() + self._gateway_info = self._gateway_device.info() # get the connected sub devices if self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU: From 4533a7759756659b59007eb99cd6070415bad180 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Jun 2021 17:09:06 +0200 Subject: [PATCH 512/750] Add MQTT select (#52120) * Add MQTT select * Fix value_template support * Lint --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/select.py | 171 +++++++ tests/components/mqtt/test_select.py | 441 ++++++++++++++++++ 4 files changed, 614 insertions(+) create mode 100644 homeassistant/components/mqtt/select.py create mode 100644 tests/components/mqtt/test_select.py diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 1c94fae9d29..a2bd7fc6b36 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -92,6 +92,7 @@ ABBREVIATIONS = { "name": "name", "off_dly": "off_delay", "on_cmd_type": "on_command_type", + "ops": "options", "opt": "optimistic", "osc_cmd_t": "oscillation_command_topic", "osc_cmd_tpl": "oscillation_command_template", diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e4f461324a9..d35065e30a8 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -45,6 +45,7 @@ SUPPORTED_COMPONENTS = [ "lock", "number", "scene", + "select", "sensor", "switch", "tag", diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py new file mode 100644 index 00000000000..310b3e508f1 --- /dev/null +++ b/homeassistant/components/mqtt/select.py @@ -0,0 +1,171 @@ +"""Configure select in a device through MQTT topic.""" +import functools +import logging + +import voluptuous as vol + +from homeassistant.components import select +from homeassistant.components.select import SelectEntity +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from . import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, + subscription, +) +from .. import mqtt +from .const import CONF_RETAIN +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper + +_LOGGER = logging.getLogger(__name__) + +CONF_OPTIONS = "options" + +DEFAULT_NAME = "MQTT Select" +DEFAULT_OPTIMISTIC = False + + +def validate_config(config): + """Validate that the configuration is valid, throws if it isn't.""" + if len(config[CONF_OPTIONS]) < 2: + raise vol.Invalid(f"'{CONF_OPTIONS}' must include at least 2 options") + + return config + + +PLATFORM_SCHEMA = vol.All( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Required(CONF_OPTIONS): cv.ensure_list, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + }, + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + validate_config, +) + + +async def async_setup_platform( + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT select through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(hass, async_add_entities, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT select dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, select.DOMAIN, setup, PLATFORM_SCHEMA) + + +async def _async_setup_entity( + hass, async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT select.""" + async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) + + +class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): + """representation of an MQTT select.""" + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the MQTT select.""" + self._config = config + self._optimistic = False + self._sub_state = None + + self._attr_current_option = None + + SelectEntity.__init__(self) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._optimistic = config[CONF_OPTIMISTIC] + self._attr_options = config[CONF_OPTIONS] + + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg): + """Handle new MQTT messages.""" + payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + payload = value_template.async_render_with_possible_json_value(payload) + + if payload not in self.options: + _LOGGER.error( + "Invalid option for %s: '%s' (valid options: %s)", + self.entity_id, + payload, + self.options, + ) + return + + self._attr_current_option = payload + self.async_write_ha_state() + + if self._config.get(CONF_STATE_TOPIC) is None: + # Force into optimistic mode. + self._optimistic = True + else: + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + } + }, + ) + + if self._optimistic: + last_state = await self.async_get_last_state() + if last_state: + self._attr_current_option = last_state.state + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + if self._optimistic: + self._attr_current_option = option + self.async_write_ha_state() + + mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + option, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py new file mode 100644 index 00000000000..41fa302a6b9 --- /dev/null +++ b/tests/components/mqtt/test_select.py @@ -0,0 +1,441 @@ +"""The tests for mqtt select component.""" +import json +from unittest.mock import patch + +import pytest + +from homeassistant.components import select +from homeassistant.components.mqtt.select import CONF_OPTIONS +from homeassistant.components.select import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + select.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test-topic", + "options": ["milk", "beer"], + } +} + + +async def test_run_select_setup(hass, mqtt_mock): + """Test that it fetches the given payload.""" + topic = "test/select" + await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "milk") + + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "milk" + + async_fire_mqtt_message(hass, topic, "beer") + + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "beer" + + +async def test_value_template(hass, mqtt_mock): + """Test that it fetches the given payload with a template.""" + topic = "test/select" + await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "value_template": "{{ value_json.val }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, '{"val":"milk"}') + + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "milk" + + async_fire_mqtt_message(hass, topic, '{"val":"beer"}') + + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "beer" + + +async def test_run_select_service_optimistic(hass, mqtt_mock): + """Test that set_value service works in optimistic mode.""" + topic = "test/select" + + fake_state = ha.State("select.test", "milk") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.state == "milk" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "beer"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "beer", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("select.test_select") + assert state.state == "beer" + + +async def test_run_select_service(hass, mqtt_mock): + """Test that set_value service works in non optimistic mode.""" + cmd_topic = "test/select/set" + state_topic = "test/select" + + assert await async_setup_component( + hass, + select.DOMAIN, + { + "select": { + "platform": "mqtt", + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, state_topic, "beer") + state = hass.states.get("select.test_select") + assert state.state == "beer" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.test_select", ATTR_OPTION: "milk"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with(cmd_topic, "milk", 0, False) + state = hass.states.get("select.test_select") + assert state.state == "beer" + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one select per unique_id.""" + config = { + select.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + { + "platform": "mqtt", + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, select.DOMAIN, config) + + +async def test_discovery_removal_select(hass, mqtt_mock, caplog): + """Test removal of discovered select.""" + data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) + await help_test_discovery_removal(hass, mqtt_mock, caplog, select.DOMAIN, data) + + +async def test_discovery_update_select(hass, mqtt_mock, caplog): + """Test update of discovered select.""" + data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + + await help_test_discovery_update( + hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + ) + + +async def test_discovery_update_unchanged_select(hass, mqtt_mock, caplog): + """Test update of discovered select.""" + data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + with patch( + "homeassistant.components.mqtt.select.MqttSelect.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, select.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + + await help_test_discovery_broken( + hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT select device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT select device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, payload="milk" + ) + + +async def test_options_attributes(hass, mqtt_mock): + """Test options attribute.""" + topic = "test/select" + await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.test_select") + assert state.attributes.get(ATTR_OPTIONS) == ["milk", "beer"] + + +async def test_invalid_options(hass, caplog, mqtt_mock): + """Test invalid options.""" + topic = "test/select" + await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": "beer", + } + }, + ) + await hass.async_block_till_done() + + assert f"'{CONF_OPTIONS}' must include at least 2 options" in caplog.text + + +async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): + """Test warning for MQTT payload which is not a valid option.""" + topic = "test/select" + await async_setup_component( + hass, + "select", + { + "select": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "öl") + + await hass.async_block_till_done() + + assert ( + "Invalid option for select.test_select: 'öl' (valid options: ['milk', 'beer'])" + in caplog.text + ) From 34a317b8476f0656a9f6221ae168c383abbdf331 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 18:48:51 +0200 Subject: [PATCH 513/750] DSMR: Device/state classes, icons, less common disabled by default (#52159) --- homeassistant/components/dsmr/const.py | 105 ++++++++++++++++++++++-- homeassistant/components/dsmr/models.py | 6 ++ homeassistant/components/dsmr/sensor.py | 32 +++----- tests/components/dsmr/test_sensor.py | 103 +++++++++++++++++++---- 4 files changed, 201 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 44d3453146b..5c75dcc61ca 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -5,6 +5,15 @@ import logging from dsmr_parser import obis_references +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, +) +from homeassistant.util import dt + from .models import DSMRSensor DOMAIN = "dsmr" @@ -33,164 +42,242 @@ DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" DEVICE_NAME_GAS = "Gas Meter" -ICON_GAS = "mdi:fire" -ICON_POWER = "mdi:flash" -ICON_POWER_FAILURE = "mdi:flash-off" -ICON_SWELL_SAG = "mdi:pulse" - - SENSORS: list[DSMRSensor] = [ DSMRSensor( name="Power Consumption", obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, + device_class=DEVICE_CLASS_POWER, force_update=True, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, + device_class=DEVICE_CLASS_POWER, force_update=True, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Tariff", obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, + icon="mdi:flash", ), DSMRSensor( name="Energy Consumption (tarif 1)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, + device_class=DEVICE_CLASS_ENERGY, force_update=True, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Consumption (tarif 2)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Production (tarif 1)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Production (tarif 2)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Consumption Phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Consumption Phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Consumption Phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Production Phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Production Phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Power Production Phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Short Power Failure Count", obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, + entity_registry_enabled_default=False, + icon="mdi:flash-off", ), DSMRSensor( name="Long Power Failure Count", obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, + entity_registry_enabled_default=False, + icon="mdi:flash-off", ), DSMRSensor( name="Voltage Sags Phase L1", obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, + entity_registry_enabled_default=False, ), DSMRSensor( name="Voltage Sags Phase L2", obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, + entity_registry_enabled_default=False, ), DSMRSensor( name="Voltage Sags Phase L3", obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, + entity_registry_enabled_default=False, ), DSMRSensor( name="Voltage Swells Phase L1", obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, + entity_registry_enabled_default=False, + icon="mdi:pulse", ), DSMRSensor( name="Voltage Swells Phase L2", obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, + entity_registry_enabled_default=False, + icon="mdi:pulse", ), DSMRSensor( name="Voltage Swells Phase L3", obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, + entity_registry_enabled_default=False, + icon="mdi:pulse", ), DSMRSensor( name="Voltage Phase L1", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, + device_class=DEVICE_CLASS_VOLTAGE, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Voltage Phase L2", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, + device_class=DEVICE_CLASS_VOLTAGE, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Voltage Phase L3", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, + device_class=DEVICE_CLASS_VOLTAGE, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Current Phase L1", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, + device_class=DEVICE_CLASS_CURRENT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Current Phase L2", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, + device_class=DEVICE_CLASS_CURRENT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Current Phase L3", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, + device_class=DEVICE_CLASS_CURRENT, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Consumption (total)", obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, dsmr_versions={"5L"}, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Production (total)", obis_reference=obis_references.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, dsmr_versions={"5L"}, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Energy Consumption (total)", obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"2.2", "4", "5", "5B"}, force_update=True, + device_class=DEVICE_CLASS_ENERGY, + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Gas Consumption", obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, - force_update=True, is_gas=True, + force_update=True, + icon="mdi:fire", + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Gas Consumption", obis_reference=obis_references.BELGIUM_HOURLY_GAS_METER_READING, dsmr_versions={"5B"}, - force_update=True, is_gas=True, + force_update=True, + icon="mdi:fire", + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), DSMRSensor( name="Gas Consumption", obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, - force_update=True, is_gas=True, + force_update=True, + icon="mdi:fire", + last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, ), ] diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py index a6568555e11..b54a5af80d5 100644 --- a/homeassistant/components/dsmr/models.py +++ b/homeassistant/components/dsmr/models.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime @dataclass @@ -11,6 +12,11 @@ class DSMRSensor: name: str obis_reference: str + device_class: str | None = None dsmr_versions: set[str] | None = None + entity_registry_enabled_default: bool = True force_update: bool = False + icon: str | None = None is_gas: bool = False + last_reset: datetime | None = None + state_class: str | None = None diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 32d0c381164..c514447b4d7 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -39,10 +39,6 @@ from .const import ( DEVICE_NAME_ENERGY, DEVICE_NAME_GAS, DOMAIN, - ICON_GAS, - ICON_POWER, - ICON_POWER_FAILURE, - ICON_SWELL_SAG, LOGGER, SENSORS, ) @@ -196,13 +192,20 @@ class DSMREntity(SensorEntity): device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS - self._attr_name = sensor.name - self._attr_force_update = sensor.force_update - self._attr_unique_id = f"{device_serial}_{sensor.name}".replace(" ", "_") + self._attr_device_class = sensor.device_class self._attr_device_info = { "identifiers": {(DOMAIN, device_serial)}, "name": device_name, } + self._attr_entity_registry_enabled_default = ( + sensor.entity_registry_enabled_default + ) + self._attr_force_update = sensor.force_update + self._attr_icon = sensor.icon + self._attr_last_reset = sensor.last_reset + self._attr_name = sensor.name + self._attr_state_class = sensor.state_class + self._attr_unique_id = f"{device_serial}_{sensor.name}".replace(" ", "_") @callback def update_data(self, telegram: dict[str, DSMRObject]) -> None: @@ -221,21 +224,6 @@ class DSMREntity(SensorEntity): dsmr_object = self.telegram[self._sensor.obis_reference] return getattr(dsmr_object, attribute, None) - @property - def icon(self) -> str | None: - """Icon to use in the frontend, if any.""" - if not self.name: - return None - if "Sags" in self.name or "Swells" in self.name: - return ICON_SWELL_SAG - if "Failure" in self.name: - return ICON_POWER_FAILURE - if "Power" in self.name: - return ICON_POWER - if "Gas" in self.name: - return ICON_GAS - return None - @property def state(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 973af27b373..90194eaeb6b 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -13,8 +13,22 @@ from unittest.mock import DEFAULT, MagicMock from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + STATE_UNKNOWN, + VOLUME_CUBIC_METERS, +) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -118,8 +132,12 @@ async def test_default_setup(hass, dsmr_connection_fixture): # make sure entities have been created and return 'unknown' state power_consumption = hass.states.get("sensor.power_consumption") - assert power_consumption.state == "unknown" - assert power_consumption.attributes.get("unit_of_measurement") is None + assert power_consumption.state == STATE_UNKNOWN + assert power_consumption.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert power_consumption.attributes.get(ATTR_ICON) is None + assert power_consumption.attributes.get(ATTR_LAST_RESET) is None + assert power_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert power_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser telegram_callback(telegram) @@ -137,12 +155,22 @@ async def test_default_setup(hass, dsmr_connection_fixture): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get("sensor.power_tariff") assert power_tariff.state == "low" - assert power_tariff.attributes.get("unit_of_measurement") == "" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None + assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + ) async def test_setup_only_energy(hass, dsmr_connection_fixture): @@ -226,12 +254,23 @@ async def test_v4_meter(hass, dsmr_connection_fixture): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get("sensor.power_tariff") assert power_tariff.state == "low" - assert power_tariff.attributes.get("unit_of_measurement") == "" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None + assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + ) async def test_v5_meter(hass, dsmr_connection_fixture): @@ -286,12 +325,22 @@ async def test_v5_meter(hass, dsmr_connection_fixture): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get("sensor.power_tariff") assert power_tariff.state == "low" - assert power_tariff.attributes.get("unit_of_measurement") == "" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None + assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + ) async def test_luxembourg_meter(hass, dsmr_connection_fixture): @@ -351,7 +400,13 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): power_tariff = hass.states.get("sensor.energy_consumption_total") assert power_tariff.state == "123.456" - assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert power_tariff.attributes.get(ATTR_ICON) is None + assert power_tariff.attributes.get(ATTR_LAST_RESET) is not None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + ) power_tariff = hass.states.get("sensor.energy_production_total") assert power_tariff.state == "654.321" @@ -360,7 +415,13 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None + assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + ) async def test_belgian_meter(hass, dsmr_connection_fixture): @@ -415,12 +476,22 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get("sensor.power_tariff") assert power_tariff.state == "normal" - assert power_tariff.attributes.get("unit_of_measurement") == "" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_consumption") assert gas_consumption.state == "745.695" - assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) is None + assert gas_consumption.attributes.get(ATTR_ICON) == "mdi:fire" + assert gas_consumption.attributes.get(ATTR_LAST_RESET) is not None + assert gas_consumption.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + ) async def test_belgian_meter_low(hass, dsmr_connection_fixture): @@ -464,7 +535,11 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): # tariff should be translated in human readable and have no unit power_tariff = hass.states.get("sensor.power_tariff") assert power_tariff.state == "low" - assert power_tariff.attributes.get("unit_of_measurement") == "" + assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert power_tariff.attributes.get(ATTR_LAST_RESET) is None + assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" async def test_tcp(hass, dsmr_connection_fixture): From 5695710463de7d098c3e9aa4509ca685eb60dc82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Jun 2021 08:15:16 -1000 Subject: [PATCH 514/750] Add mac address to samsungtv config entry data if missing (#51634) Co-authored-by: Martin Hjelmare --- .../components/samsungtv/__init__.py | 62 +++++++++++-- homeassistant/components/samsungtv/bridge.py | 53 ++++++++++- .../components/samsungtv/config_flow.py | 47 ++++------ tests/components/samsungtv/conftest.py | 2 + .../components/samsungtv/test_config_flow.py | 88 ++++++++++++++++--- tests/components/samsungtv/test_init.py | 47 ++++++++++ .../components/samsungtv/test_media_player.py | 60 ++++++++++--- 7 files changed, 293 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 31b666793af..09b513c3830 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -5,8 +5,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN +from homeassistant.config_entries import ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, + CONF_MAC, CONF_METHOD, CONF_NAME, CONF_PORT, @@ -16,8 +18,16 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .bridge import SamsungTVBridge -from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN, LOGGER +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info +from .const import ( + CONF_ON_ACTION, + DEFAULT_NAME, + DOMAIN, + LEGACY_PORT, + LOGGER, + METHOD_LEGACY, + METHOD_WEBSOCKET, +) def ensure_unique_hosts(value): @@ -90,13 +100,7 @@ async def async_setup_entry(hass, entry): """Set up the Samsung TV platform.""" # Initialize bridge - data = entry.data.copy() - bridge = _async_get_device_bridge(data) - if bridge.port is None and bridge.default_port is not None: - # For backward compat, set default port for websocket tv - data[CONF_PORT] = bridge.default_port - hass.config_entries.async_update_entry(entry, data=data) - bridge = _async_get_device_bridge(data) + bridge = await _async_create_bridge_with_updated_data(hass, entry) def stop_bridge(event): """Stop SamsungTV bridge connection.""" @@ -111,6 +115,46 @@ async def async_setup_entry(hass, entry): return True +async def _async_create_bridge_with_updated_data(hass, entry): + """Create a bridge object and update any missing data in the config entry.""" + updated_data = {} + host = entry.data[CONF_HOST] + port = entry.data.get(CONF_PORT) + method = entry.data.get(CONF_METHOD) + info = None + + if not port or not method: + if method == METHOD_LEGACY: + port = LEGACY_PORT + else: + # When we imported from yaml we didn't setup the method + # because we didn't know it + port, method, info = await async_get_device_info(hass, None, host) + if not port: + raise ConfigEntryNotReady( + "Failed to determine connection method, make sure the device is on." + ) + + updated_data[CONF_PORT] = port + updated_data[CONF_METHOD] = method + + bridge = _async_get_device_bridge({**entry.data, **updated_data}) + + if not entry.data.get(CONF_MAC) and bridge.method == METHOD_WEBSOCKET: + if info: + mac = mac_from_device_info(info) + else: + mac = await hass.async_add_executor_job(bridge.mac_from_device) + if mac: + updated_data[CONF_MAC] = mac + + if updated_data: + data = {**entry.data, **updated_data} + hass.config_entries.async_update_entry(entry, data=data) + + return bridge + + async def async_unload_entry(hass, entry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 7e1c24c6d2f..e1d3f042a45 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -17,11 +17,14 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TOKEN, ) +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DESCRIPTION, + LEGACY_PORT, LOGGER, METHOD_LEGACY, + METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, @@ -34,13 +37,44 @@ from .const import ( ) +def mac_from_device_info(info): + """Extract the mac address from the device info.""" + dev_info = info.get("device", {}) + if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): + return format_mac(dev_info["wifiMac"]) + return None + + +async def async_get_device_info(hass, bridge, host): + """Fetch the port, method, and device info.""" + return await hass.async_add_executor_job(_get_device_info, bridge, host) + + +def _get_device_info(bridge, host): + """Fetch the port, method, and device info.""" + if bridge and bridge.port: + return bridge.port, bridge.method, bridge.device_info() + + for port in WEBSOCKET_PORTS: + bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port) + if info := bridge.device_info(): + return port, METHOD_WEBSOCKET, info + + bridge = SamsungTVBridge.get_bridge(METHOD_LEGACY, host, LEGACY_PORT) + result = bridge.try_connect() + if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): + return LEGACY_PORT, METHOD_LEGACY, None + + return None, None, None + + class SamsungTVBridge(ABC): """The Base Bridge abstract class.""" @staticmethod def get_bridge(method, host, port=None, token=None): """Get Bridge instance.""" - if method == METHOD_LEGACY: + if method == METHOD_LEGACY or port == LEGACY_PORT: return SamsungTVLegacyBridge(method, host, port) return SamsungTVWSBridge(method, host, port, token) @@ -50,7 +84,6 @@ class SamsungTVBridge(ABC): self.method = method self.host = host self.token = None - self.default_port = None self._remote = None self._callback = None @@ -66,6 +99,10 @@ class SamsungTVBridge(ABC): def device_info(self): """Try to gather infos of this TV.""" + @abstractmethod + def mac_from_device(self): + """Try to fetch the mac address of the TV.""" + def is_on(self): """Tells if the TV is on.""" if self._remote: @@ -137,7 +174,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): def __init__(self, method, host, port): """Initialize Bridge.""" - super().__init__(method, host, None) + super().__init__(method, host, LEGACY_PORT) self.config = { CONF_NAME: VALUE_CONF_NAME, CONF_DESCRIPTION: VALUE_CONF_NAME, @@ -148,6 +185,10 @@ class SamsungTVLegacyBridge(SamsungTVBridge): CONF_TIMEOUT: 1, } + def mac_from_device(self): + """Try to fetch the mac address of the TV.""" + return None + def try_connect(self): """Try to connect to the Legacy TV.""" config = { @@ -212,7 +253,11 @@ class SamsungTVWSBridge(SamsungTVBridge): """Initialize Bridge.""" super().__init__(method, host, port) self.token = token - self.default_port = 8001 + + def mac_from_device(self): + """Try to fetch the mac address of the TV.""" + info = self.device_info() + return mac_from_device_info(info) if info else None def try_connect(self): """Try to connect to the Websocket TV.""" diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index e29298da2eb..4fc24c5cc3e 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.typing import DiscoveryInfoType -from .bridge import SamsungTVBridge +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( ATTR_PROPERTIES, CONF_MANUFACTURER, @@ -47,23 +47,6 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] -def _get_device_info(host): - """Fetch device info by any websocket method.""" - for port in WEBSOCKET_PORTS: - bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port) - if info := bridge.device_info(): - return info - return None - - -async def async_get_device_info(hass, bridge, host): - """Fetch device info from bridge or websocket.""" - if bridge: - return await hass.async_add_executor_job(bridge.device_info) - - return await hass.async_add_executor_job(_get_device_info, host) - - def _strip_uuid(udn): return udn[5:] if udn.startswith("uuid:") else udn @@ -107,7 +90,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_device_unique_id(self, raise_on_progress=True): """Set device unique_id.""" - await self._async_get_and_check_device_info() + if not await self._async_get_and_check_device_info(): + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) await self._async_set_unique_id_from_udn(raise_on_progress) async def _async_set_unique_id_from_udn(self, raise_on_progress=True): @@ -134,9 +118,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_and_check_device_info(self): """Try to get the device info.""" - info = await async_get_device_info(self.hass, self._bridge, self._host) + _port, _method, info = await async_get_device_info( + self.hass, self._bridge, self._host + ) if not info: - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + return False dev_info = info.get("device", {}) device_type = dev_info.get("type") if device_type != "Samsung SmartTV": @@ -146,9 +132,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._name = name.replace("[TV] ", "") if name else device_type self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) - if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): - self._mac = format_mac(dev_info.get("wifiMac")) + if mac := mac_from_device_info(info): + self._mac = mac self._device_info = info + return True async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" @@ -156,11 +143,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # since the TV may be off at startup await self._async_set_name_host_from_input(user_input) self._async_abort_entries_match({CONF_HOST: self._host}) - if user_input.get(CONF_PORT) in WEBSOCKET_PORTS: + port = user_input.get(CONF_PORT) + if port in WEBSOCKET_PORTS: user_input[CONF_METHOD] = METHOD_WEBSOCKET - else: + elif port == LEGACY_PORT: user_input[CONF_METHOD] = METHOD_LEGACY - user_input[CONF_PORT] = LEGACY_PORT user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER return self.async_create_entry( title=self._title, @@ -225,6 +212,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) + model_name = discovery_info.get(ATTR_UPNP_MODEL_NAME) self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname await self._async_set_unique_id_from_udn() @@ -234,9 +222,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "samsung" ): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) - self._name = self._title = self._model = discovery_info.get( - ATTR_UPNP_MODEL_NAME - ) + if not await self._async_get_and_check_device_info(): + # If we cannot get device info for an SSDP discovery + # its likely a legacy tv. + self._name = self._title = self._model = model_name self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 278c6d7f18a..c3da2652a6d 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -21,6 +21,7 @@ def remote_fixture(): remote = Mock() remote.__enter__ = Mock() remote.__exit__ = Mock() + remote.port.return_value = 55000 remote_class.return_value = remote yield remote @@ -37,6 +38,7 @@ def remotews_fixture(): remotews = Mock() remotews.__enter__ = Mock() remotews.__exit__ = Mock() + remotews.port.return_value = 8002 remotews.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 1dd11fa5ad9..3830673b4cc 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.samsungtv.const import ( CONF_MODEL, DEFAULT_MANUFACTURER, DOMAIN, + LEGACY_PORT, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, @@ -362,6 +363,29 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_NOT_SUPPORTED +async def test_ssdp_websocket_success_populates_mac_address( + hass: HomeAssistant, remotews: Mock +): + """Test starting a flow from ssdp for a supported device populates the mac.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" with patch( @@ -491,7 +515,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_import_legacy(hass: HomeAssistant): +async def test_import_legacy(hass: HomeAssistant, remote: Mock): """Test importing from yaml with hostname.""" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", @@ -505,14 +529,18 @@ async def test_import_legacy(hass: HomeAssistant): await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake" - assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["result"].unique_id is None + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_METHOD] == METHOD_LEGACY + assert entries[0].data[CONF_PORT] == LEGACY_PORT -async def test_import_legacy_without_name(hass: HomeAssistant): + +async def test_import_legacy_without_name(hass: HomeAssistant, remote: Mock): """Test importing from yaml without a name.""" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", @@ -526,11 +554,15 @@ async def test_import_legacy_without_name(hass: HomeAssistant): await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake_host" - assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["result"].unique_id is None + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_METHOD] == METHOD_LEGACY + assert entries[0].data[CONF_PORT] == LEGACY_PORT + async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" @@ -547,12 +579,38 @@ async def test_import_websocket(hass: HomeAssistant): assert result["type"] == "create_entry" assert result["title"] == "fake" assert result["data"][CONF_METHOD] == METHOD_WEBSOCKET + assert result["data"][CONF_PORT] == 8002 assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["result"].unique_id is None +async def test_import_websocket_without_port(hass: HomeAssistant, remotews: Mock): + """Test importing from yaml with hostname by no port.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_WSDATA, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_METHOD] == METHOD_WEBSOCKET + assert entries[0].data[CONF_PORT] == 8002 + + async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock): """Test importing from yaml with hostname that does not resolve.""" with patch( @@ -687,6 +745,7 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { "modelName": "82GXARRS", + "networkType": "wireless", "wifiMac": "aa:bb:cc:dd:ee:ff", "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "mac": "aa:bb:cc:dd:ee:ff", @@ -707,6 +766,11 @@ async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: call(**AUTODETECT_WEBSOCKET_SSL), call(**DEVICEINFO_WEBSOCKET_SSL), ] + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): @@ -747,14 +811,14 @@ async def test_autodetect_not_supported(hass: HomeAssistant, remote: Mock): async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "create_entry" - assert result["data"][CONF_METHOD] == "legacy" - assert remote.call_count == 1 - assert remote.call_args_list == [call(AUTODETECT_LEGACY)] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_METHOD] == "legacy" + assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_MAC] is None + assert result["data"][CONF_PORT] == LEGACY_PORT async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock): diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index f728fd4af10..c5c1519556d 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -8,10 +8,12 @@ from homeassistant.components.samsungtv.const import ( METHOD_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_MAC, CONF_METHOD, CONF_NAME, SERVICE_VOLUME_UP, @@ -30,6 +32,16 @@ MOCK_CONFIG = { } ] } +MOCK_CONFIG_WITHOUT_PORT = { + SAMSUNGTV_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_ON_ACTION: [{"delay": "00:00:01"}], + } + ] +} + REMOTE_CALL = { "name": "HomeAssistant", "description": "HomeAssistant", @@ -67,6 +79,41 @@ async def test_setup(hass: HomeAssistant, remote: Mock): assert remote.call_args == call(REMOTE_CALL) +async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): + """Test import from yaml when the device is offline.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + side_effect=OSError, + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + + config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(config_entries_domain) == 1 + assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_setup_from_yaml_without_port_device_online( + hass: HomeAssistant, remotews: Mock +): + """Test import from yaml when the device is online.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + + config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(config_entries_domain) == 1 + assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + + async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" DUPLICATE = { diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2cdd5cf56df..2e87f67d4e6 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -159,14 +159,33 @@ async def test_setup_websocket(hass, remotews, mock_now): remote = Mock() remote.__enter__ = Mock(return_value=enter) remote.__exit__ = Mock() + remote.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, + } remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) - assert remote_class.call_count == 1 - assert remote_class.call_args_list == [call(**MOCK_CALLS_WS)] + assert remote_class.call_count == 2 + assert remote_class.call_args_list == [ + call(**MOCK_CALLS_WS), + call(**MOCK_CALLS_WS), + ] assert hass.states.get(ENTITY_ID) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + async def test_setup_websocket_2(hass, mock_now): """Test setup of platform from config entry.""" @@ -183,20 +202,37 @@ async def test_setup_websocket_2(hass, mock_now): assert len(config_entries) == 1 assert entry is config_entries[0] - assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) - await hass.async_block_till_done() - - next_update = mock_now + timedelta(minutes=5) - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remote, patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) + with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: + enter = Mock() + type(enter).token = PropertyMock(return_value="987654321") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock() + remote.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, + } + remote_class.return_value = remote + assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) await hass.async_block_till_done() + assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state - assert remote.call_count == 1 - assert remote.call_args_list == [call(**MOCK_CALLS_WS)] + assert remote_class.call_count == 3 + assert remote_class.call_args_list[0] == call(**MOCK_CALLS_WS) async def test_update_on(hass, remote, mock_now): From fba7118d443bfb0f6f058be06bee71f40f0fbe93 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 20:25:21 +0200 Subject: [PATCH 515/750] Add Color Palette Select entities to WLED (#51994) * Add Color Palette Select entities to WLED * Update with dev changes, disable by default --- homeassistant/components/wled/__init__.py | 3 +- homeassistant/components/wled/select.py | 100 +++++++++ tests/components/wled/test_select.py | 259 ++++++++++++++++++++++ 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/wled/select.py create mode 100644 tests/components/wled/test_select.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 77b97472747..29c6b98b381 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator -PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) +PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py new file mode 100644 index 00000000000..6628334266b --- /dev/null +++ b/homeassistant/components/wled/select.py @@ -0,0 +1,100 @@ +"""Support for LED selects.""" +from __future__ import annotations + +from functools import partial + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up WLED select based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + update_segments = partial( + async_update_segments, + coordinator, + {}, + async_add_entities, + ) + coordinator.async_add_listener(update_segments) + update_segments() + + +class WLEDPaletteSelect(WLEDEntity, SelectEntity): + """Defines a WLED Palette select.""" + + _attr_icon = "mdi:palette-outline" + _segment: int + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: + """Initialize WLED .""" + super().__init__(coordinator=coordinator) + + # Segment 0 uses a simpler name, which is more natural for when using + # a single segment / using WLED with one big LED strip. + self._attr_name = ( + f"{coordinator.data.info.name} Segment {segment} Color Palette" + ) + if segment == 0: + self._attr_name = f"{coordinator.data.info.name} Color Palette" + + self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}" + self._attr_options = [ + palette.name for palette in self.coordinator.data.palettes + ] + self._segment = segment + + @property + def available(self) -> bool: + """Return True if entity is available.""" + try: + self.coordinator.data.state.segments[self._segment] + except IndexError: + return False + + return super().available + + @property + def current_option(self) -> str | None: + """Return the current selected color palette.""" + return self.coordinator.data.state.segments[self._segment].palette.name + + @wled_exception_handler + async def async_select_option(self, option: str) -> None: + """Set WLED segment to the selected color palette.""" + await self.coordinator.wled.segment(segment_id=self._segment, palette=option) + + +@callback +def async_update_segments( + coordinator: WLEDDataUpdateCoordinator, + current: dict[int, WLEDPaletteSelect], + async_add_entities, +) -> None: + """Update segments.""" + segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} + current_ids = set(current) + + new_entities = [] + + # Process new segments, add them to Home Assistant + for segment_id in segment_ids - current_ids: + current[segment_id] = WLEDPaletteSelect(coordinator, segment_id) + new_entities.append(current[segment_id]) + + if new_entities: + async_add_entities(new_entities) diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py new file mode 100644 index 00000000000..2a0817b7c12 --- /dev/null +++ b/tests/components/wled/test_select.py @@ -0,0 +1,259 @@ +"""Tests for the WLED select platform.""" +import json +from unittest.mock import MagicMock + +import pytest +from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.components.wled.const import DOMAIN, SCAN_INTERVAL +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_SELECT_OPTION, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture + + +@pytest.fixture +async def enable_all(hass: HomeAssistant) -> None: + """Enable all disabled by default select entities.""" + registry = er.async_get(hass) + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + SELECT_DOMAIN, + DOMAIN, + "aabbccddeeff_palette_0", + suggested_object_id="wled_rgb_light_color_palette", + disabled_by=None, + ) + + registry.async_get_or_create( + SELECT_DOMAIN, + DOMAIN, + "aabbccddeeff_palette_1", + suggested_object_id="wled_rgb_light_segment_1_color_palette", + disabled_by=None, + ) + + +async def test_select_state( + hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry +) -> None: + """Test the creation and values of the WLED selects.""" + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:palette-outline" + assert state.attributes.get(ATTR_OPTIONS) == [ + "Analogous", + "April Night", + "Autumn", + "Based on Primary", + "Based on Set", + "Beach", + "Beech", + "Breeze", + "C9", + "Cloud", + "Cyane", + "Default", + "Departure", + "Drywet", + "Fire", + "Forest", + "Grintage", + "Hult", + "Hult 64", + "Icefire", + "Jul", + "Landscape", + "Lava", + "Light Pink", + "Magenta", + "Magred", + "Ocean", + "Orange & Teal", + "Orangery", + "Party", + "Pastel", + "Primary Color", + "Rainbow", + "Rainbow Bands", + "Random Cycle", + "Red & Blue", + "Rewhi", + "Rivendell", + "Sakura", + "Set Colors", + "Sherbet", + "Splash", + "Sunset", + "Sunset 2", + "Tertiary", + "Tiamat", + "Vintage", + "Yelblu", + "Yellowout", + "Yelmag", + ] + assert state.state == "Random Cycle" + + entry = entity_registry.async_get("select.wled_rgb_light_segment_1_color_palette") + assert entry + assert entry.unique_id == "aabbccddeeff_palette_1" + + +async def test_segment_change_state( + hass: HomeAssistant, + enable_all: None, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the option change of state of the WLED segments.""" + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", + ATTR_OPTION: "Some Other Palette", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + segment_id=1, + palette="Some Other Palette", + ) + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_dynamically_handle_segments( + hass: HomeAssistant, + enable_all: None, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + segment0 = hass.states.get("select.wled_rgb_light_color_palette") + segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert segment0 + assert segment0.state == "Default" + assert not segment1 + + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb.json")) + ) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("select.wled_rgb_light_color_palette") + segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert segment0 + assert segment0.state == "Default" + assert segment1 + assert segment1.state == "Random Cycle" + + # Test adding if segment shows up again, including the master entity + mock_wled.update.return_value = return_value + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("select.wled_rgb_light_color_palette") + segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert segment0 + assert segment0.state == "Default" + assert segment1 + assert segment1.state == STATE_UNAVAILABLE + + +async def test_select_error( + hass: HomeAssistant, + enable_all: None, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.segment.side_effect = WLEDError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", + ATTR_OPTION: "Whatever", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert state + assert state.state == "Random Cycle" + assert "Invalid response from API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") + + +async def test_select_connection_error( + hass: HomeAssistant, + enable_all: None, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.segment.side_effect = WLEDConnectionError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", + ATTR_OPTION: "Whatever", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgb_light_segment_1_color_palette") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") + + +@pytest.mark.parametrize( + "entity_id", + ( + "select.wled_rgb_light_color_palette", + "select.wled_rgb_light_segment_1_color_palette", + ), +) +async def test_disabled_by_default_selects( + hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str +) -> None: + """Test the disabled by default WLED selects.""" + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION From a2be9a487f52c41789793f54ef61864734b91cc3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 20:27:25 +0200 Subject: [PATCH 516/750] DSMR: Complete full strictly typed (#52162) --- .strict-typing | 1 + homeassistant/components/dsmr/config_flow.py | 8 +++++--- homeassistant/components/dsmr/sensor.py | 5 +++-- mypy.ini | 11 +++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1112dfba9ae..cde4d314cca 100644 --- a/.strict-typing +++ b/.strict-typing @@ -26,6 +26,7 @@ homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.dnsip.* +homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.elgato.* homeassistant.components.fitbit.* diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 294526a386e..72e854fe43a 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -51,14 +51,16 @@ class DSMRConnection: """Equipment identifier.""" if self._equipment_identifier in self._telegram: dsmr_object = self._telegram[self._equipment_identifier] - return getattr(dsmr_object, "value", None) + identifier: str | None = getattr(dsmr_object, "value", None) + return identifier return None def equipment_identifier_gas(self) -> str | None: """Equipment identifier gas.""" if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram: dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS] - return getattr(dsmr_object, "value", None) + identifier: str | None = getattr(dsmr_object, "value", None) + return identifier return None async def validate_connect(self, hass: core.HomeAssistant) -> bool: @@ -142,7 +144,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _abort_if_host_port_configured( self, port: str, - host: str = None, + host: str | None = None, updates: dict[Any, Any] | None = None, reload_on_update: bool = True, ) -> FlowResult | None: diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index c514447b4d7..42c8dd7fd54 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -89,7 +89,7 @@ async def async_setup_entry( ) @Throttle(min_time_between_updates) - def update_entities_telegram(telegram: dict[str, DSMRObject]): + def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None: """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram for entity in entities: @@ -222,7 +222,8 @@ class DSMREntity(SensorEntity): # Get the attribute value if the object has it dsmr_object = self.telegram[self._sensor.obis_reference] - return getattr(dsmr_object, attribute, None) + attr: str | None = getattr(dsmr_object, attribute) + return attr @property def state(self) -> StateType: diff --git a/mypy.ini b/mypy.ini index 505d02b1111..c7fa78efecf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -297,6 +297,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.dsmr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.dunehd.*] check_untyped_defs = true disallow_incomplete_defs = true From e039a9d37bf428ad879ea5c9e9a8c638adb7194a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 24 Jun 2021 21:18:46 +0200 Subject: [PATCH 517/750] Tibber, correct generate a 0-timestamp (#52165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, correct generate a 0-timestamp * import Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 660bbb741b0..330e9d5c61d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,6 +1,6 @@ """Support for Tibber sensors.""" import asyncio -from datetime import datetime, timedelta +from datetime import timedelta import logging from random import randrange @@ -311,7 +311,7 @@ class TibberSensorRT(TibberSensor): "last meter consumption", "last meter production", ]: - self._attr_last_reset = datetime.fromtimestamp(0) + self._attr_last_reset = dt_util.utc_from_timestamp(0) elif self._sensor_name in [ "accumulated consumption", "accumulated production", From adade590ed90808c6c2b5a03dd1fab27a1917535 Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 24 Jun 2021 21:29:09 +0200 Subject: [PATCH 518/750] Bump pybotvac to 0.0.21 (#52166) --- homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 7632360d13c..014e366db46 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,7 +3,7 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": ["pybotvac==0.0.20"], + "requirements": ["pybotvac==0.0.21"], "codeowners": ["@dshokouhi", "@Santobert"], "dependencies": ["http"], "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index fb24a7b78c2..06b13349058 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1324,7 +1324,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.20 +pybotvac==0.0.21 # homeassistant.components.nissan_leaf pycarwings2==2.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11acc12e51e..de7cd4ed7f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -740,7 +740,7 @@ pyatv==0.7.7 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.20 +pybotvac==0.0.21 # homeassistant.components.cloudflare pycfdns==1.2.1 From a5ca25019c70f81a658925c8083289589327b18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 24 Jun 2021 21:48:09 +0200 Subject: [PATCH 519/750] Toon, correct generate a 0-timestamp (#52167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/toon/const.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 4d298f114c0..4af57e03412 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -1,5 +1,5 @@ """Constants for the Toon integration.""" -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -23,6 +23,7 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) +from homeassistant.util import dt as dt_util DOMAIN = "toon" @@ -149,7 +150,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_M3, ATTR_ICON: "mdi:gas-cylinder", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "gas_value": { @@ -196,7 +197,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "power_meter_reading_low": { @@ -206,7 +207,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "power_value": { @@ -224,7 +225,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "solar_meter_reading_low_produced": { @@ -234,7 +235,7 @@ SENSOR_ENTITIES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), ATTR_DEFAULT_ENABLED: False, }, "solar_value": { @@ -340,7 +341,7 @@ SENSOR_ENTITIES = { ATTR_ICON: "mdi:water", ATTR_DEFAULT_ENABLED: False, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_LAST_RESET: datetime.fromtimestamp(0), + ATTR_LAST_RESET: dt_util.utc_from_timestamp(0), }, "water_value": { ATTR_NAME: "Current Water Usage", From 0730b375f3ab2cf284ff6cebadc8784b236a3223 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 24 Jun 2021 21:58:37 +0200 Subject: [PATCH 520/750] Remove `air_quality` platform from Nettigo Air Monitor integration (#52152) * Remove air_quality platform * Clean constants --- homeassistant/components/nam/__init__.py | 23 ++- homeassistant/components/nam/air_quality.py | 103 ------------ homeassistant/components/nam/const.py | 52 +++++- tests/components/nam/test_air_quality.py | 173 -------------------- tests/components/nam/test_init.py | 33 +++- tests/components/nam/test_sensor.py | 111 +++++++++++++ 6 files changed, 211 insertions(+), 284 deletions(-) delete mode 100644 homeassistant/components/nam/air_quality.py delete mode 100644 tests/components/nam/test_air_quality.py diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index cf1df6b8ac8..97d54fb0669 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -14,19 +14,28 @@ from nettigo_air_monitor import ( NettigoAirMonitor, ) +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER +from .const import ( + ATTR_SDS011, + ATTR_SPS30, + DEFAULT_NAME, + DEFAULT_UPDATE_INTERVAL, + DOMAIN, + MANUFACTURER, +) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["air_quality", "sensor"] +PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -43,6 +52,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Remove air_quality entities from registry if they exist + ent_reg = entity_registry.async_get(hass) + for sensor_type in ["sds", ATTR_SDS011, ATTR_SPS30]: + unique_id = f"{coordinator.unique_id}-{sensor_type}" + if entity_id := ent_reg.async_get_entity_id( + AIR_QUALITY_PLATFORM, DOMAIN, unique_id + ): + _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) + ent_reg.async_remove(entity_id) + return True diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py deleted file mode 100644 index 4c51003f3e6..00000000000 --- a/homeassistant/components/nam/air_quality.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Support for the Nettigo Air Monitor air_quality service.""" -from __future__ import annotations - -import logging -from typing import Union, cast - -from homeassistant.components.air_quality import DOMAIN as PLATFORM, AirQualityEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import NAMDataUpdateCoordinator -from .const import ( - AIR_QUALITY_SENSORS, - ATTR_SDS011, - DEFAULT_NAME, - DOMAIN, - SUFFIX_P1, - SUFFIX_P2, -) - -PARALLEL_UPDATES = 1 - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Add a Nettigo Air Monitor entities from a config_entry.""" - coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - # Due to the change of the attribute name of one sensor, it is necessary to migrate - # the unique_id to the new name. - ent_reg = entity_registry.async_get(hass) - old_unique_id = f"{coordinator.unique_id}-sds" - new_unique_id = f"{coordinator.unique_id}-{ATTR_SDS011}" - if entity_id := ent_reg.async_get_entity_id(PLATFORM, DOMAIN, old_unique_id): - _LOGGER.debug( - "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", - entity_id, - old_unique_id, - new_unique_id, - ) - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - - entities: list[NAMAirQuality] = [] - for sensor in AIR_QUALITY_SENSORS: - if getattr(coordinator.data, f"{sensor}{SUFFIX_P1}") is not None: - entities.append(NAMAirQuality(coordinator, sensor)) - - async_add_entities(entities, False) - - -class NAMAirQuality(CoordinatorEntity, AirQualityEntity): - """Define an Nettigo Air Monitor air quality.""" - - coordinator: NAMDataUpdateCoordinator - - def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_device_info = coordinator.device_info - self._attr_name = f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[sensor_type]}" - self._attr_unique_id = f"{coordinator.unique_id}-{sensor_type}" - self.sensor_type = sensor_type - - @property - def particulate_matter_2_5(self) -> int | None: - """Return the particulate matter 2.5 level.""" - return cast( - Union[int, None], - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}"), - ) - - @property - def particulate_matter_10(self) -> int | None: - """Return the particulate matter 10 level.""" - return cast( - Union[int, None], - getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}"), - ) - - @property - def carbon_dioxide(self) -> int | None: - """Return the particulate matter 10 level.""" - return cast(Union[int, None], self.coordinator.data.mhz14a_carbon_dioxide) - - @property - def available(self) -> bool: - """Return if entity is available.""" - available = super().available - - # For a short time after booting, the device does not return values for all - # sensors. For this reason, we mark entities for which data is missing as - # unavailable. - return ( - available - and getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") - is not None - ) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 318d1e15802..f60d03eea78 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -9,6 +9,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -36,12 +38,17 @@ ATTR_DHT22_HUMIDITY: Final = "dht22_humidity" ATTR_DHT22_TEMPERATURE: Final = "dht22_temperature" ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" +ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" ATTR_SDS011: Final = "sds011" +ATTR_SDS011_P1: Final = f"{ATTR_SDS011}{SUFFIX_P1}" +ATTR_SDS011_P2: Final = f"{ATTR_SDS011}{SUFFIX_P2}" ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" ATTR_SIGNAL_STRENGTH: Final = "signal" ATTR_SPS30: Final = "sps30" ATTR_SPS30_P0: Final = f"{ATTR_SPS30}{SUFFIX_P0}" +ATTR_SPS30_P1: Final = f"{ATTR_SPS30}{SUFFIX_P1}" +ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" @@ -54,11 +61,6 @@ DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" -AIR_QUALITY_SENSORS: Final[dict[str, str]] = { - ATTR_SDS011: "SDS011", - ATTR_SPS30: "SPS30", -} - MIGRATION_SENSORS: Final = [ ("temperature", ATTR_DHT22_TEMPERATURE), ("humidity", ATTR_DHT22_HUMIDITY), @@ -121,6 +123,30 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, + ATTR_MHZ14A_CARBON_DIOXIDE: { + ATTR_LABEL: f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2, + ATTR_ICON: None, + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SDS011_P1: { + ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 10", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SDS011_P2: { + ATTR_LABEL: f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, ATTR_SHT3X_HUMIDITY: { ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", ATTR_UNIT: PERCENTAGE, @@ -145,6 +171,22 @@ SENSORS: Final[dict[str, SensorDescription]] = { ATTR_ENABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, + ATTR_SPS30_P1: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 10", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + ATTR_SPS30_P2: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, ATTR_SPS30_P4: { ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py deleted file mode 100644 index 5f687b8a29a..00000000000 --- a/tests/components/nam/test_air_quality.py +++ /dev/null @@ -1,173 +0,0 @@ -"""Test air_quality of Nettigo Air Monitor integration.""" -from datetime import timedelta -from unittest.mock import patch - -from nettigo_air_monitor import ApiError - -from homeassistant.components.air_quality import ( - ATTR_CO2, - ATTR_PM_2_5, - ATTR_PM_10, - DOMAIN as AIR_QUALITY_DOMAIN, -) -from homeassistant.components.nam.const import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - STATE_UNAVAILABLE, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from . import INCOMPLETE_NAM_DATA, nam_data - -from tests.common import async_fire_time_changed -from tests.components.nam import init_integration - - -async def test_air_quality(hass): - """Test states of the air_quality.""" - await init_integration(hass) - registry = er.async_get(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == "11" - assert state.attributes.get(ATTR_PM_10) == 19 - assert state.attributes.get(ATTR_PM_2_5) == 11 - assert state.attributes.get(ATTR_CO2) == 865 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" - - state = hass.states.get("air_quality.nettigo_air_monitor_sps30") - assert state - assert state.state == "34" - assert state.attributes.get(ATTR_PM_10) == 21 - assert state.attributes.get(ATTR_PM_2_5) == 34 - assert state.attributes.get(ATTR_CO2) == 865 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30" - - -async def test_air_quality_without_co2_value(hass): - """Test states of the air_quality.""" - await init_integration(hass, co2_sensor=False) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.attributes.get(ATTR_CO2) is None - - -async def test_incompleta_data_after_device_restart(hass): - """Test states of the air_quality after device restart.""" - await init_integration(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == "11" - assert state.attributes.get(ATTR_PM_10) == 19 - assert state.attributes.get(ATTR_PM_2_5) == 11 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - - future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=INCOMPLETE_NAM_DATA, - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == STATE_UNAVAILABLE - - -async def test_availability(hass): - """Ensure that we mark the entities unavailable correctly when device causes an error.""" - await init_integration(hass) - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "11" - - future = utcnow() + timedelta(minutes=6) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - side_effect=ApiError("API Error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state == STATE_UNAVAILABLE - - future = utcnow() + timedelta(minutes=12) - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "11" - - -async def test_manual_update_entity(hass): - """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) - - await async_setup_component(hass, "homeassistant", {}) - - with patch( - "homeassistant.components.nam.NettigoAirMonitor._async_get_data", - return_value=nam_data, - ) as mock_get_data: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["air_quality.nettigo_air_monitor_sds011"]}, - blocking=True, - ) - - assert mock_get_data.call_count == 1 - - -async def test_unique_id_migration(hass): - """Test states of the unique_id migration.""" - registry = er.async_get(hass) - - registry.async_get_or_create( - AIR_QUALITY_DOMAIN, - DOMAIN, - "aa:bb:cc:dd:ee:ff-sds", - suggested_object_id="nettigo_air_monitor_sds011", - disabled_by=None, - ) - - await init_integration(hass) - - entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") - assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011" diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 943ea53f360..97392cbaff8 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -3,9 +3,11 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry from tests.components.nam import init_integration @@ -15,7 +17,7 @@ async def test_async_setup_entry(hass): """Test a successful setup entry.""" await init_integration(hass) - state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == "11" @@ -51,3 +53,32 @@ async def test_unload_entry(hass): assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_remove_air_quality_entities(hass): + """Test remove air_quality entities from registry.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "aa:bb:cc:dd:ee:ff-sds011", + suggested_object_id="nettigo_air_monitor_sds011", + disabled_by=None, + ) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "aa:bb:cc:dd:ee:ff-sps30", + suggested_object_id="nettigo_air_monitor_sps30", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") + assert entry is None + + entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") + assert entry is None diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 12fa0f01e44..506a81f7619 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -13,7 +13,11 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -205,6 +209,113 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_10" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") + assert state + assert state.state == "31" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p0" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") + assert state + assert state.state == "21" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.nettigo_air_monitor_sps30_particulate_matter_10") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_2_5") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_4_0") + assert state + assert state.state == "25" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get( + "sensor.nettigo_air_monitor_sps30_particulate_matter_4_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_p4" + + state = hass.states.get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") + assert state + assert state.state == "865" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CO2 + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_MILLION + ) + entry = registry.async_get("sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide" + async def test_sensor_disabled(hass): """Test sensor disabled by default.""" From f9d65b9196db78856efe84f4c14e9382dfbbe033 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Jun 2021 23:16:07 +0200 Subject: [PATCH 521/750] Add preset support to WLED (#52170) --- homeassistant/components/wled/light.py | 8 -- homeassistant/components/wled/select.py | 36 ++++++ tests/components/wled/test_select.py | 131 +++++++++++++++++++- tests/fixtures/wled/rgbw.json | 157 +++++++++++++++++++++++- 4 files changed, 317 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 0cb45caab87..65866935562 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -5,7 +5,6 @@ from functools import partial from typing import Any, Tuple, cast import voluptuous as vol -from wled import Preset from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -218,18 +217,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if playlist == -1: playlist = None - preset: int | None = None - if isinstance(self.coordinator.data.state.preset, Preset): - preset = self.coordinator.data.state.preset.preset_id - elif self.coordinator.data.state.preset != -1: - preset = self.coordinator.data.state.preset - segment = self.coordinator.data.state.segments[self._segment] return { ATTR_INTENSITY: segment.intensity, ATTR_PALETTE: segment.palette.name, ATTR_PLAYLIST: playlist, - ATTR_PRESET: preset, ATTR_REVERSE: segment.reverse, ATTR_SPEED: segment.speed, } diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 6628334266b..845ff38b5e6 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -3,6 +3,8 @@ from __future__ import annotations from functools import partial +from wled import Preset + from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -23,6 +25,9 @@ async def async_setup_entry( ) -> None: """Set up WLED select based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([WLEDPresetSelect(coordinator)]) + update_segments = partial( async_update_segments, coordinator, @@ -33,6 +38,37 @@ async def async_setup_entry( update_segments() +class WLEDPresetSelect(WLEDEntity, SelectEntity): + """Defined a WLED Preset select.""" + + _attr_icon = "mdi:playlist-play" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED .""" + super().__init__(coordinator=coordinator) + + self._attr_name = f"{coordinator.data.info.name} Preset" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" + self._attr_options = [preset.name for preset in self.coordinator.data.presets] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return len(self.coordinator.data.presets) > 0 and super().available + + @property + def current_option(self) -> str | None: + """Return the current selected preset.""" + if not isinstance(self.coordinator.data.state.preset, Preset): + return None + return self.coordinator.data.state.preset.name + + @wled_exception_handler + async def async_select_option(self, option: str) -> None: + """Set WLED segment to the selected preset.""" + await self.coordinator.wled.preset(preset=option) + + class WLEDPaletteSelect(WLEDEntity, SelectEntity): """Defines a WLED Palette select.""" diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index 2a0817b7c12..abdef0c2ff5 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_ICON, SERVICE_SELECT_OPTION, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -44,7 +45,7 @@ async def enable_all(hass: HomeAssistant) -> None: ) -async def test_select_state( +async def test_color_palette_state( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry ) -> None: """Test the creation and values of the WLED selects.""" @@ -113,7 +114,7 @@ async def test_select_state( assert entry.unique_id == "aabbccddeeff_palette_1" -async def test_segment_change_state( +async def test_color_palette_segment_change_state( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -138,7 +139,7 @@ async def test_segment_change_state( @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) -async def test_dynamically_handle_segments( +async def test_color_palette_dynamically_handle_segments( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -179,7 +180,7 @@ async def test_dynamically_handle_segments( assert segment1.state == STATE_UNAVAILABLE -async def test_select_error( +async def test_color_palette_select_error( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -208,7 +209,7 @@ async def test_select_error( mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") -async def test_select_connection_error( +async def test_color_palette_select_connection_error( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -237,6 +238,126 @@ async def test_select_connection_error( mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") +async def test_preset_unavailable_without_presets( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test WLED preset entity is unavailable when presets are not available.""" + state = hass.states.get("select.wled_rgb_light_preset") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the creation and values of the WLED selects.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:playlist-play" + assert state.attributes.get(ATTR_OPTIONS) == ["Preset 1", "Preset 2"] + assert state.state == "Preset 1" + + entry = entity_registry.async_get("select.wled_rgbw_light_preset") + assert entry + assert entry.unique_id == "aabbccddee11_preset" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_old_style_preset_active( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test unknown preset returned (when old style/unknown) preset is active.""" + # Set device preset state to a random number + mock_wled.update.return_value.state.preset = 99 + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.preset.side_effect = WLEDError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == "Preset 1" + assert "Invalid response from API" in caplog.text + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_select_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.preset.side_effect = WLEDConnectionError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + @pytest.mark.parametrize( "entity_id", ( diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json index ce7033c5888..d5ba9e8d00c 100644 --- a/tests/fixtures/wled/rgbw.json +++ b/tests/fixtures/wled/rgbw.json @@ -3,7 +3,7 @@ "on": true, "bri": 140, "transition": 7, - "ps": -1, + "ps": 1, "pl": -1, "nl": { "on": false, @@ -200,5 +200,158 @@ "Orangery", "C9", "Sakura" - ] + ], + "presets": { + "0": {}, + "1": { + "on": false, + "bri": 255, + "transition": 7, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "grp": 1, + "spc": 0, + "on": true, + "bri": 255, + "col": [ + [ + 97, + 144, + 255 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ] + ], + "fx": 9, + "sx": 183, + "ix": 255, + "pal": 1, + "sel": true, + "rev": false, + "mi": false + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + } + ], + "n": "Preset 1" + }, + "2": { + "on": false, + "bri": 255, + "transition": 7, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "grp": 1, + "spc": 0, + "on": true, + "bri": 255, + "col": [ + [ + 97, + 144, + 255 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ] + ], + "fx": 9, + "sx": 183, + "ix": 255, + "pal": 1, + "sel": true, + "rev": false, + "mi": false + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + } + ], + "n": "Preset 2" + } + } } From febc276db9a404071bacb74c9dd956bda08ff3fc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 25 Jun 2021 00:12:31 +0000 Subject: [PATCH 522/750] [ci skip] Translation update --- .../components/dsmr/translations/ca.json | 38 ++++++++++- .../components/dsmr/translations/et.json | 36 +++++++++- .../components/dsmr/translations/nl.json | 34 +++++++++- .../components/dsmr/translations/ru.json | 38 ++++++++++- .../select/translations/zh-Hans.json | 14 ++++ .../xiaomi_miio/translations/zh-Hans.json | 67 ++++++++++++++++++- .../components/zwave_js/translations/et.json | 50 ++++++++++++++ .../components/zwave_js/translations/ru.json | 11 +++ 8 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/select/translations/zh-Hans.json diff --git a/homeassistant/components/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json index a876776fea2..263cb388980 100644 --- a/homeassistant/components/dsmr/translations/ca.json +++ b/homeassistant/components/dsmr/translations/ca.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_communicate": "No s'ha pogut comunicar", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_communicate": "No s'ha pogut comunicar", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "Selecciona la versi\u00f3 DSMR", + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Selecciona l'adre\u00e7a de connexi\u00f3" + }, + "setup_serial": { + "data": { + "dsmr_version": "Selecciona la versi\u00f3 DSMR", + "port": "Selecciona el dispositiu" + }, + "title": "Dispositiu" + }, + "setup_serial_manual_path": { + "data": { + "port": "Ruta del port USB del dispositiu" + }, + "title": "Ruta" + }, + "user": { + "data": { + "type": "Tipus de connexi\u00f3" + }, + "title": "Selecciona el tipus de connexi\u00f3" + } } }, "options": { diff --git a/homeassistant/components/dsmr/translations/et.json b/homeassistant/components/dsmr/translations/et.json index 67f37f26586..5e9131621d3 100644 --- a/homeassistant/components/dsmr/translations/et.json +++ b/homeassistant/components/dsmr/translations/et.json @@ -1,15 +1,47 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_communicate": "\u00dchendamine nurjus", + "cannot_connect": "\u00dchendamine nurjus" }, "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_communicate": "\u00dchendamine nurjus", + "cannot_connect": "\u00dchendamine nurjus", "one": "\u00fcks", "other": "mitu" }, "step": { "one": "\u00fcks", - "other": "mitu" + "other": "mitu", + "setup_network": { + "data": { + "dsmr_version": "Vali DSMR versioon", + "host": "Host", + "port": "Port" + }, + "title": "Vali \u00fchenduse aadress" + }, + "setup_serial": { + "data": { + "dsmr_version": "Vali DSMR versioon", + "port": "Vali seade" + }, + "title": "Seade" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB-seadme asukoha rada" + }, + "title": "Rada" + }, + "user": { + "data": { + "type": "\u00dchenduse t\u00fc\u00fcp" + }, + "title": "Vali \u00fchenduse t\u00fc\u00fcp" + } } }, "options": { diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json index ba31fa36fd2..8827bed5fee 100644 --- a/homeassistant/components/dsmr/translations/nl.json +++ b/homeassistant/components/dsmr/translations/nl.json @@ -1,15 +1,45 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" }, "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "one": "Leeg", "other": "Leeg" }, "step": { "one": "Leeg", - "other": "Leeg" + "other": "Leeg", + "setup_network": { + "data": { + "dsmr_version": "Selecteer DSMR-versie", + "host": "Host", + "port": "Poort" + }, + "title": "Selecteer verbindingsadres" + }, + "setup_serial": { + "data": { + "dsmr_version": "Selecteer DSMR-versie", + "port": "Selecteer apparaat" + }, + "title": "Apparaat" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB-apparaatpad" + }, + "title": "Pad" + }, + "user": { + "data": { + "type": "Verbindingstype" + }, + "title": "Selecteer verbindingstype" + } } }, "options": { diff --git a/homeassistant/components/dsmr/translations/ru.json b/homeassistant/components/dsmr/translations/ru.json index 3bf0cf9f06f..0ee5d273156 100644 --- a/homeassistant/components/dsmr/translations/ru.json +++ b/homeassistant/components/dsmr/translations/ru.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_communicate": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_communicate": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e DSMR", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "setup_serial": { + "data": { + "dsmr_version": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e DSMR", + "port": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "setup_serial_manual_path": { + "data": { + "port": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "\u041f\u0443\u0442\u044c" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + } } }, "options": { diff --git a/homeassistant/components/select/translations/zh-Hans.json b/homeassistant/components/select/translations/zh-Hans.json new file mode 100644 index 00000000000..4857f798f3b --- /dev/null +++ b/homeassistant/components/select/translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "\u6539\u53d8 {entity_name} \u7684\u9009\u9879" + }, + "condition_type": { + "selected_option": "{entity_name} \u5f53\u524d\u9009\u62e9\u9879" + }, + "trigger_type": { + "current_option_changed": "{entity_name} \u7684\u9009\u9879\u53d8\u5316" + } + }, + "title": "\u9009\u5b9a" +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index 1f84cd3de5a..0034f73fbf3 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -2,14 +2,47 @@ "config": { "abort": { "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", - "already_in_progress": "\u6b64\u5c0f\u7c73\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002" + "already_in_progress": "\u6b64\u5c0f\u7c73\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002", + "incomplete_info": "\u8bbe\u5907\u4fe1\u606f\u4e0d\u5b8c\u6574\uff0c\u672a\u63d0\u4f9b IP \u6216 token\u3002", + "not_xiaomi_miio": "Xiaomi Miio \u6682\u672a\u9002\u914d\u8be5\u8bbe\u5907\u3002" }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002" + "cloud_credentials_incomplete": "\u4e91\u7aef\u51ed\u636e\u4e0d\u5b8c\u6574\uff0c\u8bf7\u8f93\u5165\u7528\u6237\u540d\u3001\u5bc6\u7801\u548c\u56fd\u5bb6/\u5730\u533a", + "cloud_login_error": "\u65e0\u6cd5\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\uff0c\u8bf7\u68c0\u67e5\u51ed\u636e\u3002", + "cloud_no_devices": "\u672a\u5728\u5c0f\u7c73\u5e10\u6237\u4e2d\u53d1\u73b0\u8bbe\u5907\u3002", + "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002", + "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002" }, "flow_title": "Xiaomi Miio: {name}", "step": { + "cloud": { + "data": { + "cloud_country": "\u4e91\u670d\u52a1\u56fd\u5bb6/\u5730\u533a", + "cloud_password": "\u5bc6\u7801", + "cloud_username": "\u7528\u6237\u540d", + "manual": "\u624b\u52a8\u914d\u7f6e\uff08\u4e0d\u63a8\u8350\uff09" + }, + "description": "\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\u3002\u6709\u5173\u56fd\u5bb6/\u5730\u533a\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.openhab.org/addons/bindings/miio/#country-servers \u3002", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" + }, + "connect": { + "data": { + "model": "\u8bbe\u5907 model" + }, + "description": "\u4ece\u652f\u6301\u7684\u578b\u53f7\u4e2d\u624b\u52a8\u9009\u62e9\u3002", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" + }, + "device": { + "data": { + "host": "IP \u5730\u5740", + "model": "\u8bbe\u5907 model\uff08\u53ef\u9009\uff09", + "name": "\u8bbe\u5907\u540d\u79f0", + "token": "API Token" + }, + "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" + }, "gateway": { "data": { "host": "IP \u5730\u5740", @@ -19,6 +52,22 @@ "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002", "title": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" }, + "manual": { + "data": { + "host": "IP \u5730\u5740", + "token": "API Token" + } + }, + "reauth_confirm": { + "description": "\u5c0f\u7c73 Miio \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237\uff0c\u4ee5\u4fbf\u66f4\u65b0 token \u6216\u6dfb\u52a0\u4e22\u5931\u7684\u4e91\u7aef\u51ed\u636e\u3002" + }, + "select": { + "data": { + "select_device": "Miio \u8bbe\u5907" + }, + "description": "\u9009\u62e9\u8981\u6dfb\u52a0\u7684\u5c0f\u7c73 Miio \u8bbe\u5907\u3002", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" + }, "user": { "data": { "gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" @@ -27,5 +76,19 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "\u4e91\u7aef\u51ed\u636e\u4e0d\u5b8c\u6574\uff0c\u8bf7\u8f93\u5165\u7528\u6237\u540d\u3001\u5bc6\u7801\u548c\u56fd\u5bb6/\u5730\u533a" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u901a\u8fc7\u4e91\u7aef\u83b7\u53d6\u8fde\u63a5\u7684\u5b50\u8bbe\u5907" + }, + "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 2ae0a0f47c6..434a39e61d7 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", + "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.", + "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", + "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", + "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "different_device": "\u00dchendatud USB-seade ei ole sama, mis on varem selle seadekande jaoks seadistatud. Selle asemel loo uue seadme jaoks uus seadekanne." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_ws_url": "Vale sihtkoha aadress", + "unknown": "Tundmatu t\u00f5rge" + }, + "progress": { + "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit.", + "start_addon": "Palun oota kuni Z-Wave JS lisandmooduli ak\u00e4ivitumine l\u00f5ppeb. See v\u00f5ib v\u00f5tta m\u00f5ned sekundid." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Riistvara emuleerimine", + "log_level": "Logimise tase", + "network_key": "V\u00f5rgu v\u00f5ti", + "usb_path": "USB-seadme asukoha rada" + }, + "title": "Sisesta Z-Wave JS lisandmooduli seaded" + }, + "install_addon": { + "title": "Z-Wave JS lisandmooduli paigaldamine on alanud" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Kasuta Z-Wave JS Supervisori lisandmoodulit" + }, + "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?", + "title": "Vali \u00fchendusviis" + }, + "start_addon": { + "title": "Z-Wave JS lisandmoodul k\u00e4ivitub." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index b0b3745fac4..1655a84190d 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -51,5 +51,16 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", + "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", + "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", + "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.", + "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + } + }, "title": "Z-Wave JS" } \ No newline at end of file From d009f06a55ae1e8761ae6522ac67b9806ac79a1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Jun 2021 19:31:44 -1000 Subject: [PATCH 523/750] Handle connection being closed in legacy samsungtv (#52137) * Handle connection being closed in legacy samsungtv - Mirror the websocket behavior Fixes ``` 2021-06-24 02:54:13 ERROR (MainThread) [homeassistant.helpers.entity] Update for media_player.89_guestroom fails Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 401, in async_update_ha_state await self.async_device_update() File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 609, in async_device_update raise exc File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "/usr/src/homeassistant/homeassistant/components/samsungtv/media_player.py", line 124, in update self._state = STATE_ON if self._bridge.is_on() else STATE_OFF File "/usr/src/homeassistant/homeassistant/components/samsungtv/bridge.py", line 113, in is_on return self._get_remote() is not None File "/usr/src/homeassistant/homeassistant/components/samsungtv/bridge.py", line 232, in _get_remote self._remote = Remote(self.config.copy()) File "/usr/local/lib/python3.8/site-packages/samsungctl/remote.py", line 9, in __init__ self.remote = RemoteLegacy(config) File "/usr/local/lib/python3.8/site-packages/samsungctl/remote_legacy.py", line 32, in __init__ self._read_response(True) File "/usr/local/lib/python3.8/site-packages/samsungctl/remote_legacy.py", line 77, in _read_response raise exceptions.ConnectionClosed() samsungctl.exceptions.ConnectionClosed ``` * add coverage * pass instead --- homeassistant/components/samsungtv/bridge.py | 2 ++ .../components/samsungtv/test_media_player.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index e1d3f042a45..1cdd63acd3c 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -234,6 +234,8 @@ class SamsungTVLegacyBridge(SamsungTVBridge): except AccessDenied: self._notify_callback() raise + except (ConnectionClosed, OSError): + pass return self._remote def _send_key(self, key): diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2e87f67d4e6..de9183915a2 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -340,6 +340,32 @@ async def test_update_unhandled_response(hass, remote, mock_now): assert state.state == STATE_ON +async def test_connection_closed_during_update_can_recover(hass, remote, mock_now): + """Testing update tv connection closed exception can recover.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=[exceptions.ConnectionClosed(), DEFAULT_MOCK], + ): + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key(hass, remote): """Test for send key.""" await setup_samsungtv(hass, MOCK_CONFIG) From 22c8afe637a98a82c79f2b636c81473663e7bbc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Jun 2021 19:39:21 -1000 Subject: [PATCH 524/750] Create a base class for broadlink entities (#52132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create a base class for broadlink entities * Update homeassistant/components/broadlink/entity.py Co-authored-by: Daniel Hjelseth Høyer * Update homeassistant/components/broadlink/entity.py * Update homeassistant/components/broadlink/entity.py Co-authored-by: Daniel Hjelseth Høyer * black, remove unused Co-authored-by: Daniel Hjelseth Høyer --- homeassistant/components/broadlink/device.py | 5 +++ homeassistant/components/broadlink/entity.py | 32 ++++++++++++++++++++ homeassistant/components/broadlink/remote.py | 26 ++-------------- homeassistant/components/broadlink/sensor.py | 26 ++-------------- homeassistant/components/broadlink/switch.py | 26 ++-------------- 5 files changed, 46 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/broadlink/entity.py diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index b18d64c327f..2686b3dd9ed 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -51,6 +51,11 @@ class BroadlinkDevice: """Return the unique id of the device.""" return self.config.unique_id + @property + def mac_address(self): + """Return the mac address of the device.""" + return self.config.data[CONF_MAC] + @staticmethod async def async_update(hass, entry): """Update the device and related entities. diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py new file mode 100644 index 00000000000..850611b391f --- /dev/null +++ b/homeassistant/components/broadlink/entity.py @@ -0,0 +1,32 @@ +"""Broadlink entities.""" + +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN + + +class BroadlinkEntity: + """Representation of a Broadlink entity.""" + + _attr_should_poll = False + + def __init__(self, device): + """Initialize the device.""" + self._device = device + + @property + def available(self): + """Return True if the remote is available.""" + return self._device.update_manager.available + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "connections": {(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, + "manufacturer": self._device.api.manufacturer, + "model": self._device.api.model, + "name": self._device.name, + "sw_version": self._device.fw_version, + } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 291bf6a3d8b..3e0c37d3f55 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -40,6 +40,7 @@ from homeassistant.helpers.storage import Store from homeassistant.util.dt import utcnow from .const import DOMAIN +from .entity import BroadlinkEntity from .helpers import data_packet, import_device _LOGGER = logging.getLogger(__name__) @@ -112,12 +113,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([remote], False) -class BroadlinkRemote(RemoteEntity, RestoreEntity): +class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """Representation of a Broadlink remote.""" def __init__(self, device, codes, flags): """Initialize the remote.""" - self._device = device + super().__init__(device) self._coordinator = device.update_manager.coordinator self._code_storage = codes self._flag_storage = flags @@ -142,32 +143,11 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): """Return True if the remote is on.""" return self._state - @property - def available(self): - """Return True if the remote is available.""" - return self._device.update_manager.available - - @property - def should_poll(self): - """Return True if the remote has to be polled for state.""" - return False - @property def supported_features(self): """Flag supported features.""" return SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "manufacturer": self._device.api.manufacturer, - "model": self._device.api.model, - "name": self._device.name, - "sw_version": self._device.fw_version, - } - def _extract_codes(self, commands, device=None): """Extract a list of codes. diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 92708583c43..15445699a33 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -16,6 +16,7 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from .const import DOMAIN +from .entity import BroadlinkEntity from .helpers import import_device _LOGGER = logging.getLogger(__name__) @@ -67,12 +68,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class BroadlinkSensor(SensorEntity): +class BroadlinkSensor(BroadlinkEntity, SensorEntity): """Representation of a Broadlink sensor.""" def __init__(self, device, monitored_condition): """Initialize the sensor.""" - self._device = device + super().__init__(device) self._coordinator = device.update_manager.coordinator self._monitored_condition = monitored_condition self._state = self._coordinator.data[monitored_condition] @@ -92,21 +93,11 @@ class BroadlinkSensor(SensorEntity): """Return the state of the sensor.""" return self._state - @property - def available(self): - """Return True if the sensor is available.""" - return self._device.update_manager.available - @property def unit_of_measurement(self): """Return the unit of measurement of the sensor.""" return SENSOR_TYPES[self._monitored_condition][1] - @property - def should_poll(self): - """Return True if the sensor has to be polled for state.""" - return False - @property def device_class(self): """Return device class.""" @@ -117,17 +108,6 @@ class BroadlinkSensor(SensorEntity): """Return state class.""" return SENSOR_TYPES[self._monitored_condition][3] - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "manufacturer": self._device.api.manufacturer, - "model": self._device.api.model, - "name": self._device.name, - "sw_version": self._device.fw_version, - } - @callback def update_data(self): """Update data.""" diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 0a98530c806..4aed6e2288a 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from .const import DOMAIN, SWITCH_DOMAIN +from .entity import BroadlinkEntity from .helpers import data_packet, import_device, mac_address _LOGGER = logging.getLogger(__name__) @@ -131,12 +132,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(switches) -class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): +class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): """Representation of a Broadlink switch.""" def __init__(self, device, command_on, command_off): """Initialize the switch.""" - self._device = device + super().__init__(device) self._command_on = command_on self._command_off = command_off self._coordinator = device.update_manager.coordinator @@ -152,37 +153,16 @@ class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): """Return True if unable to access real state of the switch.""" return True - @property - def available(self): - """Return True if the switch is available.""" - return self._device.update_manager.available - @property def is_on(self): """Return True if the switch is on.""" return self._state - @property - def should_poll(self): - """Return True if the switch has to be polled for state.""" - return False - @property def device_class(self): """Return device class.""" return DEVICE_CLASS_SWITCH - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "manufacturer": self._device.api.manufacturer, - "model": self._device.api.model, - "name": self._device.name, - "sw_version": self._device.fw_version, - } - @callback def update_data(self): """Update data.""" From e6c850136cfd81051b8f993e1f882ebf72aa05ef Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 25 Jun 2021 10:06:15 +0200 Subject: [PATCH 525/750] Add support for state_class to AccuWeather integration (#51510) * Add support for state_class * Use get() method --- homeassistant/components/accuweather/const.py | 13 +++++++ homeassistant/components/accuweather/model.py | 3 +- .../components/accuweather/sensor.py | 3 +- tests/components/accuweather/test_sensor.py | 34 ++++++++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index e834feae8d2..aea394446ad 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Final +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -233,6 +234,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "Ceiling": { ATTR_DEVICE_CLASS: None, @@ -241,6 +243,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: LENGTH_METERS, ATTR_UNIT_IMPERIAL: LENGTH_FEET, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "CloudCover": { ATTR_DEVICE_CLASS: None, @@ -249,6 +252,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: PERCENTAGE, ATTR_UNIT_IMPERIAL: PERCENTAGE, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "DewPoint": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -257,6 +261,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "RealFeelTemperature": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -265,6 +270,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "RealFeelTemperatureShade": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -273,6 +279,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "Precipitation": { ATTR_DEVICE_CLASS: None, @@ -281,6 +288,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, ATTR_UNIT_IMPERIAL: LENGTH_INCHES, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "PressureTendency": { ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", @@ -297,6 +305,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: UV_INDEX, ATTR_UNIT_IMPERIAL: UV_INDEX, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "WetBulbTemperature": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -305,6 +314,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "WindChillTemperature": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -313,6 +323,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "Wind": { ATTR_DEVICE_CLASS: None, @@ -321,6 +332,7 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_ENABLED: True, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, "WindGust": { ATTR_DEVICE_CLASS: None, @@ -329,5 +341,6 @@ SENSOR_TYPES: Final[dict[str, SensorDescription]] = { ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, ATTR_ENABLED: False, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, } diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py index cc51efbd0e2..2127629728b 100644 --- a/homeassistant/components/accuweather/model.py +++ b/homeassistant/components/accuweather/model.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TypedDict -class SensorDescription(TypedDict): +class SensorDescription(TypedDict, total=False): """Sensor description class.""" device_class: str | None @@ -13,3 +13,4 @@ class SensorDescription(TypedDict): unit_metric: str | None unit_imperial: str | None enabled: bool + state_class: str | None diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index bf6b0efd6c2..ba99df14d9e 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any, cast -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -89,6 +89,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self._device_class = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self.forecast_day = forecast_day + self._attr_state_class = self._description.get(ATTR_STATE_CLASS) @property def name(self) -> str: diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 64c49c61fe7..62f282f2cf3 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -4,7 +4,11 @@ import json from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -43,6 +47,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_cloud_ceiling") assert entry @@ -55,6 +60,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy" assert state.attributes.get("type") is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_precipitation") assert entry @@ -66,6 +72,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:gauge" assert state.attributes.get(ATTR_DEVICE_CLASS) == "accuweather__pressure_tendency" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_pressure_tendency") assert entry @@ -77,6 +84,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_realfeel_temperature") assert entry @@ -88,6 +96,7 @@ async def test_sensor_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX assert state.attributes.get("level") == "High" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_uv_index") assert entry @@ -105,6 +114,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_HOURS + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_hours_of_sun_0d") assert entry @@ -116,6 +126,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_max_0d") assert entry @@ -126,6 +137,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_min_0d") assert entry @@ -137,6 +149,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_thunderstorm_probability_day_0d") assert entry @@ -148,6 +161,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_thunderstorm_probability_night_0d") assert entry @@ -160,6 +174,7 @@ async def test_sensor_with_forecast(hass): assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX assert state.attributes.get("level") == "Moderate" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_uv_index_0d") assert entry @@ -346,6 +361,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_apparent_temperature") assert entry @@ -357,6 +373,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_cloud_cover") assert entry @@ -368,6 +385,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_dew_point") assert entry @@ -379,6 +397,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_realfeel_temperature_shade") assert entry @@ -390,6 +409,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_wet_bulb_temperature") assert entry @@ -401,6 +421,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_wind_chill_temperature") assert entry @@ -412,6 +433,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_wind_gust") assert entry @@ -423,6 +445,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT entry = registry.async_get("sensor.home_wind") assert entry @@ -434,6 +457,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_cloud_cover_day_0d") assert entry @@ -445,6 +469,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_cloud_cover_night_0d") assert entry @@ -459,6 +484,7 @@ async def test_sensor_enabled_without_forecast(hass): ) assert state.attributes.get("level") == "Low" assert state.attributes.get(ATTR_ICON) == "mdi:grass" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_grass_pollen_0d") assert entry @@ -485,6 +511,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get("level") == "Good" assert state.attributes.get(ATTR_ICON) == "mdi:vector-triangle" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_ozone_0d") assert entry @@ -511,6 +538,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_0d") assert entry @@ -537,6 +565,7 @@ async def test_sensor_enabled_without_forecast(hass): ) assert state.attributes.get("level") == "Low" assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_tree_pollen_0d") assert entry @@ -561,6 +590,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get("direction") == "WNW" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_wind_night_0d") assert entry @@ -573,6 +603,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get("direction") == "S" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_wind_gust_day_0d") assert entry @@ -585,6 +616,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR assert state.attributes.get("direction") == "WSW" assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_wind_gust_night_0d") assert entry From b939570c9cf8196e828640bbfd002e08237e16d1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Jun 2021 10:57:12 +0200 Subject: [PATCH 526/750] Simplify WLED segment tracking (#52174) * Simplify WLED segment tracking * Fix master controls --- homeassistant/components/wled/light.py | 25 +++++++++---------------- homeassistant/components/wled/select.py | 9 ++++----- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 65866935562..4326f1066c7 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -86,9 +86,8 @@ async def async_setup_entry( update_segments = partial( async_update_segments, - entry, coordinator, - {}, + set(), async_add_entities, ) @@ -363,30 +362,24 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): @callback def async_update_segments( - entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight | WLEDMasterLight], + current_ids: set[int], async_add_entities, ) -> None: """Update segments.""" segment_ids = {light.segment_id for light in coordinator.data.state.segments} - current_ids = set(current) - new_entities = [] - - # Discard master (if present) - current_ids.discard(-1) - - # Process new segments, add them to Home Assistant - for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDSegmentLight(coordinator, segment_id) - new_entities.append(current[segment_id]) + new_entities: list[WLEDMasterLight | WLEDSegmentLight] = [] # More than 1 segment now? No master? Add master controls if not coordinator.keep_master_light and ( len(current_ids) < 2 and len(segment_ids) > 1 ): - current[-1] = WLEDMasterLight(coordinator) - new_entities.append(current[-1]) + new_entities.append(WLEDMasterLight(coordinator)) + + # Process new segments, add them to Home Assistant + for segment_id in segment_ids - current_ids: + current_ids.add(segment_id) + new_entities.append(WLEDSegmentLight(coordinator, segment_id)) if new_entities: async_add_entities(new_entities) diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 845ff38b5e6..373565b7ef7 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -31,7 +31,7 @@ async def async_setup_entry( update_segments = partial( async_update_segments, coordinator, - {}, + set(), async_add_entities, ) coordinator.async_add_listener(update_segments) @@ -118,19 +118,18 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): @callback def async_update_segments( coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDPaletteSelect], + current_ids: set[int], async_add_entities, ) -> None: """Update segments.""" segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} - current_ids = set(current) new_entities = [] # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDPaletteSelect(coordinator, segment_id) - new_entities.append(current[segment_id]) + current_ids.add(segment_id) + new_entities.append(WLEDPaletteSelect(coordinator, segment_id)) if new_entities: async_add_entities(new_entities) From 958016c44f5f961f97fc791b782aeb127c4f775c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Jun 2021 11:57:52 +0200 Subject: [PATCH 527/750] Clean up input_boolean, removing typing exceptions (#52181) * Clean up input_boolean, removing typing exceptions * Now pushing all local changes... --- .../components/input_boolean/__init__.py | 55 ++++++++----------- mypy.ini | 3 - script/hassfest/mypy_config.py | 1 - 3 files changed, 22 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 661d58da989..f198d552781 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -77,7 +78,7 @@ class InputBooleanStorageCollection(collection.StorageCollection): @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Test if input_boolean is True.""" return hass.states.is_state(entity_id, STATE_ON) @@ -144,14 +145,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, config: dict | None) -> None: + _attr_should_poll = False + + def __init__(self, config: ConfigType) -> None: """Initialize a boolean input.""" self._config = config self.editable = True - self._state = config.get(CONF_INITIAL) + self._attr_is_on = config.get(CONF_INITIAL, False) + self._attr_unique_id = config[CONF_ID] @classmethod - def from_yaml(cls, config: dict) -> InputBoolean: + def from_yaml(cls, config: ConfigType) -> InputBoolean: """Return entity instance initialized from yaml storage.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -159,56 +163,41 @@ class InputBoolean(ToggleEntity, RestoreEntity): return input_bool @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): + def name(self) -> str | None: """Return name of the boolean input.""" return self._config.get(CONF_NAME) @property - def extra_state_attributes(self): - """Return the state attributes of the entity.""" - return {ATTR_EDITABLE: self.editable} - - @property - def icon(self): + def icon(self) -> str | None: """Return the icon to be used for this entity.""" return self._config.get(CONF_ICON) @property - def is_on(self): - """Return true if entity is on.""" - return self._state + def extra_state_attributes(self) -> dict[str, bool]: + """Return the state attributes of the entity.""" + return {ATTR_EDITABLE: self.editable} - @property - def unique_id(self): - """Return a unique ID for the person.""" - return self._config[CONF_ID] - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" - # If not None, we got an initial value. + # Don't restore if we got an initial value. await super().async_added_to_hass() - if self._state is not None: + if self._config.get(CONF_INITIAL) is not None: return state = await self.async_get_last_state() - self._state = state and state.state == STATE_ON + self._attr_is_on = state is not None and state.state == STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - self._state = True + self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - self._state = False + self._attr_is_on = False self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/mypy.ini b/mypy.ini index c7fa78efecf..0c4fe1bf7a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1229,9 +1229,6 @@ ignore_errors = true [mypy-homeassistant.components.influxdb.*] ignore_errors = true -[mypy-homeassistant.components.input_boolean.*] -ignore_errors = true - [mypy-homeassistant.components.input_datetime.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 21600257ffb..601e6b55845 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -101,7 +101,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.image.*", "homeassistant.components.incomfort.*", "homeassistant.components.influxdb.*", - "homeassistant.components.input_boolean.*", "homeassistant.components.input_datetime.*", "homeassistant.components.input_number.*", "homeassistant.components.insteon.*", From a95294c83fc01db0653f2921792ebcdd18ab6a9a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 25 Jun 2021 12:07:32 +0200 Subject: [PATCH 528/750] Fix typo in Nettigo Air Monitor integration (#52182) --- homeassistant/components/nam/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index e088a8f00c1..ae3d1e639d5 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -41,7 +41,7 @@ async def async_setup_entry( """Add a Nettigo Air Monitor entities from a config_entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - # Due to the change of the attribute name of two sensora, it is necessary to migrate + # Due to the change of the attribute name of two sensors, it is necessary to migrate # the unique_ids to the new names. ent_reg = entity_registry.async_get(hass) for old_sensor, new_sensor in MIGRATION_SENSORS: From bc2689fd75bcec393ccd8079a73042ed6d3a3468 Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Fri, 25 Jun 2021 13:59:51 +0200 Subject: [PATCH 529/750] Add day-consumption fixed cost sensor in dsmr_reader (#52178) * Added day-consumption fixed cost property * black Co-authored-by: Franck Nijhof --- homeassistant/components/dsmr_reader/definitions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index daf6b9eb950..d403f84e9b9 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -268,6 +268,12 @@ DEFINITIONS = { "icon": "mdi:currency-eur", "unit": CURRENCY_EURO, }, + "dsmr/day-consumption/fixed_cost": { + "name": "Current day fixed cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, "dsmr/meter-stats/dsmr_version": { "name": "DSMR version", "enable_default": True, From 3b0f67acd1c0ffaca947889c498874777475d484 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Jun 2021 14:42:06 +0200 Subject: [PATCH 530/750] DSMR: Add deprecation warning for YAML configuration (#52179) --- homeassistant/components/dsmr/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 42c8dd7fd54..cfdcbd95cf4 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -64,6 +64,12 @@ async def async_setup_platform( discovery_info: dict[str, Any] | None = None, ) -> None: """Import the platform into a config entry.""" + LOGGER.warning( + "Configuration of the DSMR platform in YAML is deprecated and will be " + "removed in Home Assistant 2021.9; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From dad7a597ae5d0dcd2e9431aab7d7c29b88e91b6c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Jun 2021 17:37:15 +0200 Subject: [PATCH 531/750] Add color_mode support to yeelight light (#51973) * Add color_mode support to yeelight light * Satisfy pylint * Address review comment * Improve test coverage * Improve test coverage --- homeassistant/components/yeelight/light.py | 99 ++++--- tests/components/yeelight/__init__.py | 4 +- tests/components/yeelight/test_light.py | 308 +++++++++++++++++++-- 3 files changed, 350 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 6a4b7e85001..8d0a3b0ffd4 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -18,11 +18,14 @@ from homeassistant.components.light import ( ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_UNKNOWN, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, @@ -62,13 +65,7 @@ from . import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_YEELIGHT = ( - SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT -) - -SUPPORT_YEELIGHT_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP - -SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR +SUPPORT_YEELIGHT = SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT ATTR_MINUTES = "minutes" @@ -273,7 +270,7 @@ async def async_setup_entry( elif device_type == BulbType.Color: if nl_switch_light and device.is_nightlight_supported: _lights_setup_helper(YeelightColorLightWithNightlightSwitch) - _lights_setup_helper(YeelightNightLightModeWithWithoutBrightnessControl) + _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl) else: _lights_setup_helper(YeelightColorLightWithoutNightlightSwitch) elif device_type == BulbType.WhiteTemp: @@ -398,6 +395,9 @@ def _async_setup_services(hass: HomeAssistant): class YeelightGenericLight(YeelightEntity, LightEntity): """Representation of a Yeelight generic light.""" + _attr_color_mode = COLOR_MODE_BRIGHTNESS + _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + def __init__(self, device, entry, custom_effects=None): """Initialize the Yeelight light.""" super().__init__(device, entry) @@ -406,6 +406,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self._color_temp = None self._hs = None + self._rgb = None self._effect = None model_specs = self._bulb.get_model_specs() @@ -503,6 +504,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return the color property.""" return self._hs + @property + def rgb_color(self) -> tuple: + """Return the color property.""" + return self._rgb + @property def effect(self): """Return the current effect.""" @@ -558,32 +564,30 @@ class YeelightGenericLight(YeelightEntity, LightEntity): def update(self): """Update light properties.""" self._hs = self._get_hs_from_properties() + self._rgb = self._get_rgb_from_properties() if not self.device.is_color_flow_enabled: self._effect = None def _get_hs_from_properties(self): - rgb = self._get_property("rgb") - color_mode = self._get_property("color_mode") - - if not rgb or not color_mode: + hue = self._get_property("hue") + sat = self._get_property("sat") + if hue is None or sat is None: return None - color_mode = int(color_mode) - if color_mode == 2: # color temperature - temp_in_k = mired_to_kelvin(self.color_temp) - return color_util.color_temperature_to_hs(temp_in_k) - if color_mode == 3: # hsv - hue = int(self._get_property("hue")) - sat = int(self._get_property("sat")) + return (int(hue), int(sat)) - return (hue / 360 * 65536, sat / 100 * 255) + def _get_rgb_from_properties(self): + rgb = self._get_property("rgb") + + if rgb is None: + return None rgb = int(rgb) blue = rgb & 0xFF green = (rgb >> 8) & 0xFF red = (rgb >> 16) & 0xFF - return color_util.color_RGB_to_hs(red, green, blue) + return (red, green, blue) def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" @@ -606,10 +610,19 @@ class YeelightGenericLight(YeelightEntity, LightEntity): brightness / 255 * 100, duration=duration, light_type=self.light_type ) + @_cmd + def set_hs(self, hs_color, duration) -> None: + """Set bulb's color.""" + if hs_color and COLOR_MODE_HS in self.supported_color_modes: + _LOGGER.debug("Setting HS: %s", hs_color) + self._bulb.set_hsv( + hs_color[0], hs_color[1], duration=duration, light_type=self.light_type + ) + @_cmd def set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" - if rgb and self.supported_features & SUPPORT_COLOR: + if rgb and COLOR_MODE_RGB in self.supported_color_modes: _LOGGER.debug("Setting RGB: %s", rgb) self._bulb.set_rgb( rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type @@ -618,7 +631,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @_cmd def set_colortemp(self, colortemp, duration) -> None: """Set bulb's color temperature.""" - if colortemp and self.supported_features & SUPPORT_COLOR_TEMP: + if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) _LOGGER.debug("Setting color temp: %s K", temp_in_k) @@ -702,7 +715,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) hs_color = kwargs.get(ATTR_HS_COLOR) - rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None + rgb = kwargs.get(ATTR_RGB_COLOR) flash = kwargs.get(ATTR_FLASH) effect = kwargs.get(ATTR_EFFECT) @@ -726,6 +739,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): try: # values checked for none in methods + self.set_hs(hs_color, duration) self.set_rgb(rgb, duration) self.set_colortemp(colortemp, duration) self.set_brightness(brightness, duration) @@ -786,13 +800,23 @@ class YeelightGenericLight(YeelightEntity, LightEntity): _LOGGER.error("Unable to set scene: %s", ex) -class YeelightColorLightSupport: +class YeelightColorLightSupport(YeelightGenericLight): """Representation of a Color Yeelight light support.""" + _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_RGB} + @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_YEELIGHT_RGB + def color_mode(self): + """Return the color mode.""" + color_mode = int(self._get_property("color_mode")) + if color_mode == 1: # RGB + return COLOR_MODE_RGB + if color_mode == 2: # color temperature + return COLOR_MODE_COLOR_TEMP + if color_mode == 3: # hsv + return COLOR_MODE_HS + _LOGGER.debug("Light reported unknown color mode: %s", color_mode) + return COLOR_MODE_UNKNOWN @property def _predefined_effects(self): @@ -800,12 +824,10 @@ class YeelightColorLightSupport: class YeelightWhiteTempLightSupport: - """Representation of a Color Yeelight light.""" + """Representation of a White temp Yeelight light.""" - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_YEELIGHT_WHITE_TEMP + _attr_color_mode = COLOR_MODE_COLOR_TEMP + _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} @property def _predefined_effects(self): @@ -913,12 +935,15 @@ class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): return "main_power" -class YeelightNightLightModeWithWithoutBrightnessControl(YeelightNightLightMode): +class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode): """Representation of a Yeelight, when in nightlight mode. It represents case when nightlight mode brightness control is not supported. """ + _attr_color_mode = COLOR_MODE_ONOFF + _attr_supported_color_modes = {COLOR_MODE_ONOFF} + @property def supported_features(self): """Flag no supported features.""" diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index d6306e273ff..5725880f942 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -49,7 +49,9 @@ PROPERTIES = { "bg_flowing": "0", "bg_ct": "5000", "bg_bright": "80", - "bg_rgb": "16711680", + "bg_rgb": "65280", + "bg_hue": "200", + "bg_sat": "70", "nl_br": "23", "active_mode": "0", "current_brightness": "30", diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index a001f099062..9283514cb70 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -77,8 +77,6 @@ from homeassistant.components.yeelight.light import ( SERVICE_SET_MUSIC_MODE, SERVICE_START_FLOW, SUPPORT_YEELIGHT, - SUPPORT_YEELIGHT_RGB, - SUPPORT_YEELIGHT_WHITE_TEMP, YEELIGHT_COLOR_EFFECT_LIST, YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST, @@ -171,7 +169,91 @@ async def test_services(hass: HomeAssistant, caplog): == err_count + 1 ) - # turn_on + # turn_on rgb_color + brightness = 100 + rgb_color = (0, 128, 255) + transition = 2 + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS: brightness, + ATTR_RGB_COLOR: rgb_color, + ATTR_FLASH: FLASH_LONG, + ATTR_EFFECT: EFFECT_STOP, + ATTR_TRANSITION: transition, + }, + blocking=True, + ) + mocked_bulb.turn_on.assert_called_once_with( + duration=transition * 1000, + light_type=LightType.Main, + power_mode=PowerMode.NORMAL, + ) + mocked_bulb.turn_on.reset_mock() + mocked_bulb.start_music.assert_called_once() + mocked_bulb.start_music.reset_mock() + mocked_bulb.set_brightness.assert_called_once_with( + brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main + ) + mocked_bulb.set_brightness.reset_mock() + mocked_bulb.set_color_temp.assert_not_called() + mocked_bulb.set_color_temp.reset_mock() + mocked_bulb.set_hsv.assert_not_called() + mocked_bulb.set_hsv.reset_mock() + mocked_bulb.set_rgb.assert_called_once_with( + *rgb_color, duration=transition * 1000, light_type=LightType.Main + ) + mocked_bulb.set_rgb.reset_mock() + mocked_bulb.start_flow.assert_called_once() # flash + mocked_bulb.start_flow.reset_mock() + mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.stop_flow.reset_mock() + + # turn_on hs_color + brightness = 100 + hs_color = (180, 100) + transition = 2 + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS: brightness, + ATTR_HS_COLOR: hs_color, + ATTR_FLASH: FLASH_LONG, + ATTR_EFFECT: EFFECT_STOP, + ATTR_TRANSITION: transition, + }, + blocking=True, + ) + mocked_bulb.turn_on.assert_called_once_with( + duration=transition * 1000, + light_type=LightType.Main, + power_mode=PowerMode.NORMAL, + ) + mocked_bulb.turn_on.reset_mock() + mocked_bulb.start_music.assert_called_once() + mocked_bulb.start_music.reset_mock() + mocked_bulb.set_brightness.assert_called_once_with( + brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main + ) + mocked_bulb.set_brightness.reset_mock() + mocked_bulb.set_color_temp.assert_not_called() + mocked_bulb.set_color_temp.reset_mock() + mocked_bulb.set_hsv.assert_called_once_with( + *hs_color, duration=transition * 1000, light_type=LightType.Main + ) + mocked_bulb.set_hsv.reset_mock() + mocked_bulb.set_rgb.assert_not_called() + mocked_bulb.set_rgb.reset_mock() + mocked_bulb.start_flow.assert_called_once() # flash + mocked_bulb.start_flow.reset_mock() + mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) + mocked_bulb.stop_flow.reset_mock() + + # turn_on color_temp brightness = 100 color_temp = 200 transition = 1 @@ -203,6 +285,8 @@ async def test_services(hass: HomeAssistant, caplog): duration=transition * 1000, light_type=LightType.Main, ) + mocked_bulb.set_hsv.assert_not_called() + mocked_bulb.set_rgb.assert_not_called() mocked_bulb.start_flow.assert_called_once() # flash mocked_bulb.stop_flow.assert_called_once_with(light_type=LightType.Main) @@ -331,12 +415,12 @@ async def test_services(hass: HomeAssistant, caplog): ) -async def test_device_types(hass: HomeAssistant): +async def test_device_types(hass: HomeAssistant, caplog): """Test different device types.""" mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") - properties["color_mode"] = "3" + properties["color_mode"] = "3" # HSV mocked_bulb.last_properties = properties async def _async_setup(config_entry): @@ -403,15 +487,16 @@ async def test_device_types(hass: HomeAssistant): ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) hue = int(PROPERTIES["hue"]) sat = int(PROPERTIES["sat"]) - hs_color = (round(hue / 360 * 65536, 3), round(sat / 100 * 255, 3)) - rgb_color = color_hs_to_RGB(*hs_color) - xy_color = color_hs_to_xy(*hs_color) + rgb = int(PROPERTIES["rgb"]) + rgb_color = ((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF) + hs_color = (hue, sat) bg_bright = round(255 * int(PROPERTIES["bg_bright"]) / 100) bg_ct = color_temperature_kelvin_to_mired(int(PROPERTIES["bg_ct"])) + bg_hue = int(PROPERTIES["bg_hue"]) + bg_sat = int(PROPERTIES["bg_sat"]) bg_rgb = int(PROPERTIES["bg_rgb"]) + bg_hs_color = (bg_hue, bg_sat) bg_rgb_color = ((bg_rgb >> 16) & 0xFF, (bg_rgb >> 8) & 0xFF, bg_rgb & 0xFF) - bg_hs_color = color_RGB_to_hs(*bg_rgb_color) - bg_xy_color = color_RGB_to_xy(*bg_rgb_color) nl_br = round(255 * int(PROPERTIES["nl_br"]) / 100) # Default @@ -440,14 +525,15 @@ async def test_device_types(hass: HomeAssistant): }, ) - # Color + # Color - color mode CT + mocked_bulb.last_properties["color_mode"] = "2" # CT model_specs = _MODEL_SPECS["color"] await _async_test( BulbType.Color, "color", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, - "supported_features": SUPPORT_YEELIGHT_RGB, + "supported_features": SUPPORT_YEELIGHT, "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -456,11 +542,8 @@ async def test_device_types(hass: HomeAssistant): ), "brightness": current_brightness, "color_temp": ct, - "hs_color": hs_color, - "rgb_color": rgb_color, - "xy_color": xy_color, - "color_mode": "hs", - "supported_color_modes": ["color_temp", "hs"], + "color_mode": "color_temp", + "supported_color_modes": ["color_temp", "hs", "rgb"], }, { "supported_features": 0, @@ -469,6 +552,144 @@ async def test_device_types(hass: HomeAssistant): }, ) + # Color - color mode HS + mocked_bulb.last_properties["color_mode"] = "3" # HSV + model_specs = _MODEL_SPECS["color"] + await _async_test( + BulbType.Color, + "color", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": current_brightness, + "hs_color": hs_color, + "rgb_color": color_hs_to_RGB(*hs_color), + "xy_color": color_hs_to_xy(*hs_color), + "color_mode": "hs", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], + }, + ) + + # Color - color mode RGB + mocked_bulb.last_properties["color_mode"] = "1" # RGB + model_specs = _MODEL_SPECS["color"] + await _async_test( + BulbType.Color, + "color", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": current_brightness, + "hs_color": color_RGB_to_hs(*rgb_color), + "rgb_color": rgb_color, + "xy_color": color_RGB_to_xy(*rgb_color), + "color_mode": "rgb", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], + }, + ) + + # Color - color mode HS but no hue + mocked_bulb.last_properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties["hue"] = None + model_specs = _MODEL_SPECS["color"] + await _async_test( + BulbType.Color, + "color", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": current_brightness, + "color_mode": "hs", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], + }, + ) + + # Color - color mode RGB but no color + mocked_bulb.last_properties["color_mode"] = "1" # RGB + mocked_bulb.last_properties["rgb"] = None + model_specs = _MODEL_SPECS["color"] + await _async_test( + BulbType.Color, + "color", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": current_brightness, + "color_mode": "rgb", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], + }, + ) + + # Color - unsupported color_mode + mocked_bulb.last_properties["color_mode"] = 4 # Unsupported + model_specs = _MODEL_SPECS["color"] + await _async_test( + BulbType.Color, + "color", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "color_mode": "unknown", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], + }, + ) + assert "Light reported unknown color mode: 4" in caplog.text + # WhiteTemp model_specs = _MODEL_SPECS["ceiling1"] await _async_test( @@ -476,7 +697,7 @@ async def test_device_types(hass: HomeAssistant): "ceiling1", { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, - "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP, + "supported_features": SUPPORT_YEELIGHT, "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -509,7 +730,7 @@ async def test_device_types(hass: HomeAssistant): "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "flowing": False, "night_light": True, - "supported_features": SUPPORT_YEELIGHT_WHITE_TEMP, + "supported_features": SUPPORT_YEELIGHT, "min_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["max"] ), @@ -529,21 +750,62 @@ async def test_device_types(hass: HomeAssistant): "supported_color_modes": ["brightness"], }, ) + # Background light - color mode CT + mocked_bulb.last_properties["bg_lmode"] = "2" # CT await _async_test( BulbType.WhiteTempMood, "ceiling4", { "effect_list": YEELIGHT_COLOR_EFFECT_LIST, - "supported_features": SUPPORT_YEELIGHT_RGB, + "supported_features": SUPPORT_YEELIGHT, "min_mireds": color_temperature_kelvin_to_mired(6500), "max_mireds": color_temperature_kelvin_to_mired(1700), "brightness": bg_bright, "color_temp": bg_ct, + "color_mode": "color_temp", + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + name=f"{UNIQUE_NAME} ambilight", + entity_id=f"{ENTITY_LIGHT}_ambilight", + ) + + # Background light - color mode HS + mocked_bulb.last_properties["bg_lmode"] = "3" # HS + await _async_test( + BulbType.WhiteTempMood, + "ceiling4", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired(6500), + "max_mireds": color_temperature_kelvin_to_mired(1700), + "brightness": bg_bright, "hs_color": bg_hs_color, - "rgb_color": bg_rgb_color, - "xy_color": bg_xy_color, + "rgb_color": color_hs_to_RGB(*bg_hs_color), + "xy_color": color_hs_to_xy(*bg_hs_color), "color_mode": "hs", - "supported_color_modes": ["color_temp", "hs"], + "supported_color_modes": ["color_temp", "hs", "rgb"], + }, + name=f"{UNIQUE_NAME} ambilight", + entity_id=f"{ENTITY_LIGHT}_ambilight", + ) + + # Background light - color mode RGB + mocked_bulb.last_properties["bg_lmode"] = "1" # RGB + await _async_test( + BulbType.WhiteTempMood, + "ceiling4", + { + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired(6500), + "max_mireds": color_temperature_kelvin_to_mired(1700), + "brightness": bg_bright, + "hs_color": color_RGB_to_hs(*bg_rgb_color), + "rgb_color": bg_rgb_color, + "xy_color": color_RGB_to_xy(*bg_rgb_color), + "color_mode": "rgb", + "supported_color_modes": ["color_temp", "hs", "rgb"], }, name=f"{UNIQUE_NAME} ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", From 328ab21a05081032bd0eecd30364eb630dc6b728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 25 Jun 2021 21:14:26 +0200 Subject: [PATCH 532/750] Stream requests to ingress (#52184) --- homeassistant/components/hassio/ingress.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 61cec64bfda..b0c6e9d1dbe 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -7,7 +7,7 @@ import logging import os import aiohttp -from aiohttp import hdrs, web +from aiohttp import ClientTimeout, hdrs, web from aiohttp.web_exceptions import HTTPBadGateway from multidict import CIMultiDict @@ -117,7 +117,6 @@ class HassIOIngress(HomeAssistantView): ) -> web.Response | web.StreamResponse: """Ingress route for request.""" url = self._create_url(token, path) - data = await request.read() source_header = _init_header(request, token) async with self._websession.request( @@ -126,7 +125,8 @@ class HassIOIngress(HomeAssistantView): headers=source_header, params=request.query, allow_redirects=False, - data=data, + data=request.content, + timeout=ClientTimeout(total=None), ) as result: headers = _response_header(result) @@ -168,6 +168,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st if name in ( hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING, + hdrs.TRANSFER_ENCODING, hdrs.SEC_WEBSOCKET_EXTENSIONS, hdrs.SEC_WEBSOCKET_PROTOCOL, hdrs.SEC_WEBSOCKET_VERSION, From 6bbe477d662dd71b064d98aabc0db2438dcc06f5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 25 Jun 2021 21:25:51 +0200 Subject: [PATCH 533/750] Improve Xiaomi Miio error handling (#52009) * Xiaomi Miio inprove error logging * improve error handeling * fix styling * fix styling * Update homeassistant/components/xiaomi_miio/device.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Martin Hjelmare * break long line * Clean up Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/device.py | 13 +++- .../components/xiaomi_miio/gateway.py | 65 +++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index cb91726ecad..081b910efdb 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -1,8 +1,10 @@ """Code to handle a Xiaomi Device.""" import logging +from construct.core import ChecksumError from miio import Device, DeviceException +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import Entity @@ -33,17 +35,24 @@ class ConnectXiaomiDevice: async def async_connect_device(self, host, token): """Connect to the Xiaomi Device.""" _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + try: self._device = Device(host, token) # get the device info self._device_info = await self._hass.async_add_executor_job( self._device.info ) - except DeviceException: + except DeviceException as error: + if isinstance(error.__cause__, ChecksumError): + raise ConfigEntryAuthFailed(error) from error + _LOGGER.error( - "DeviceException during setup of xiaomi device with host %s", host + "DeviceException during setup of xiaomi device with host %s: %s", + host, + error, ) return False + _LOGGER.debug( "%s %s %s detected", self._device_info.model, diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 7dd84cf7812..17f42f4bffa 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -1,6 +1,7 @@ """Code to handle a Xiaomi Gateway.""" import logging +from construct.core import ChecksumError from micloud import MiCloud from miio import DeviceException, gateway from miio.gateway.gateway import GATEWAY_MODEL_EU @@ -75,19 +76,44 @@ class ConnectXiaomiGateway: self._gateway_device = gateway.Gateway(self._host, self._token) # get the gateway info self._gateway_info = self._gateway_device.info() + except DeviceException as error: + if isinstance(error.__cause__, ChecksumError): + raise ConfigEntryAuthFailed(error) from error - # get the connected sub devices - if self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU: - if ( - self._cloud_username is None - or self._cloud_password is None - or self._cloud_country is None - ): - raise ConfigEntryAuthFailed( - "Missing cloud credentials in Xiaomi Miio configuration" - ) + _LOGGER.error( + "DeviceException during setup of xiaomi gateway with host %s: %s", + self._host, + error, + ) + return False - # use miio-cloud + # get the connected sub devices + use_cloud = self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU + if not use_cloud: + # use local query (not supported by all gateway types) + try: + self._gateway_device.discover_devices() + except DeviceException as error: + _LOGGER.info( + "DeviceException during getting subdevices of xiaomi gateway" + " with host %s, trying cloud to obtain subdevices: %s", + self._host, + error, + ) + use_cloud = True + + if use_cloud: + # use miio-cloud + if ( + self._cloud_username is None + or self._cloud_password is None + or self._cloud_country is None + ): + raise ConfigEntryAuthFailed( + "Missing cloud credentials in Xiaomi Miio configuration" + ) + + try: miio_cloud = MiCloud(self._cloud_username, self._cloud_password) if not miio_cloud.login(): raise ConfigEntryAuthFailed( @@ -95,16 +121,13 @@ class ConnectXiaomiGateway: ) devices_raw = miio_cloud.get_devices(self._cloud_country) self._gateway_device.get_devices_from_dict(devices_raw) - else: - # use local query (not supported by all gateway types) - self._gateway_device.discover_devices() - - except DeviceException: - _LOGGER.error( - "DeviceException during setup of xiaomi gateway with host %s", - self._host, - ) - return False + except DeviceException as error: + _LOGGER.error( + "DeviceException during setup of xiaomi gateway with host %s: %s", + self._host, + error, + ) + return False return True From a71af8e9d3f2e46e114b49a642ec86039627c05b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Jun 2021 10:31:33 -1000 Subject: [PATCH 534/750] Abort samsungtv config flow for existing hosts when the unique id is set (#52138) Co-authored-by: Martin Hjelmare --- .../components/samsungtv/config_flow.py | 65 +++++++++----- .../components/samsungtv/test_config_flow.py | 90 +++++++++++++++---- 2 files changed, 116 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 4fc24c5cc3e..76128a1f1dd 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -51,6 +51,11 @@ def _strip_uuid(udn): return udn[5:] if udn.startswith("uuid:") else udn +def _entry_is_complete(entry): + """Return True if the config entry information is complete.""" + return bool(entry.unique_id and entry.data.get(CONF_MAC)) + + class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" @@ -93,12 +98,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not await self._async_get_and_check_device_info(): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) await self._async_set_unique_id_from_udn(raise_on_progress) + self._async_update_and_abort_for_matching_unique_id() async def _async_set_unique_id_from_udn(self, raise_on_progress=True): """Set the unique id from the udn.""" assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) - self._async_update_existing_host_entry(self._host) + if (entry := self._async_update_existing_host_entry()) and _entry_is_complete( + entry + ): + raise data_entry_flow.AbortFlow("already_configured") + + def _async_update_and_abort_for_matching_unique_id(self): + """Abort and update host and mac if we have it.""" updates = {CONF_HOST: self._host} if self._mac: updates[CONF_MAC] = self._mac @@ -178,37 +190,50 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) @callback - def _async_update_existing_host_entry(self, host): + def _async_update_existing_host_entry(self): + """Check existing entries and update them. + + Returns the existing entry if it was updated. + """ for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] != host: + if entry.data[CONF_HOST] != self._host: continue entry_kw_args = {} if self.unique_id and entry.unique_id is None: entry_kw_args["unique_id"] = self.unique_id if self._mac and not entry.data.get(CONF_MAC): - data_copy = dict(entry.data) - data_copy[CONF_MAC] = self._mac - entry_kw_args["data"] = data_copy + entry_kw_args["data"] = {**entry.data, CONF_MAC: self._mac} if entry_kw_args: self.hass.config_entries.async_update_entry(entry, **entry_kw_args) - return entry + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return entry return None - async def _async_start_discovery(self): + async def _async_start_discovery_with_mac_address(self): """Start discovery.""" assert self._host is not None - if entry := self._async_update_existing_host_entry(self._host): - if entry.unique_id: - # Let the flow continue to fill the missing - # unique id as we may be able to obtain it - # in the next step - raise data_entry_flow.AbortFlow("already_configured") + if (entry := self._async_update_existing_host_entry()) and entry.unique_id: + # If we have the unique id and the mac we abort + # as we do not need anything else + raise data_entry_flow.AbortFlow("already_configured") + self._async_abort_if_host_already_in_progress() + @callback + def _async_abort_if_host_already_in_progress(self): self.context[CONF_HOST] = self._host for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self._host: raise data_entry_flow.AbortFlow("already_in_progress") + @callback + def _abort_if_manufacturer_is_not_samsung(self): + if not self._manufacturer or not self._manufacturer.lower().startswith( + "samsung" + ): + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) @@ -216,16 +241,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) self._host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname await self._async_set_unique_id_from_udn() - await self._async_start_discovery() self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] - if not self._manufacturer or not self._manufacturer.lower().startswith( - "samsung" - ): - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + self._abort_if_manufacturer_is_not_samsung() if not await self._async_get_and_check_device_info(): # If we cannot get device info for an SSDP discovery # its likely a legacy tv. self._name = self._title = self._model = model_name + self._async_update_and_abort_for_matching_unique_id() + self._async_abort_if_host_already_in_progress() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -234,7 +257,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info[MAC_ADDRESS] self._host = discovery_info[IP_ADDRESS] - await self._async_start_discovery() + await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -244,7 +267,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) self._host = discovery_info[CONF_HOST] - await self._async_start_discovery() + await self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 3830673b4cc..a0d2875ca59 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -894,12 +894,22 @@ async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=MOCK_DHCP_DATA, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" @@ -910,18 +920,53 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data=MOCK_ZEROCONF_DATA, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +async def test_update_missing_mac_unique_id_added_from_ssdp(hass, remotews: Mock): + """Test missing mac and unique id added via ssdp.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( hass, remotews: Mock ): @@ -932,12 +977,21 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data=MOCK_ZEROCONF_DATA, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" From 42c431762897c124310c2a02dc5bf20e099bf568 Mon Sep 17 00:00:00 2001 From: PeteBa Date: Fri, 25 Jun 2021 22:29:38 +0100 Subject: [PATCH 535/750] Avoid drift in recorder purge cut-off (#52135) --- homeassistant/components/recorder/__init__.py | 16 +-- homeassistant/components/recorder/purge.py | 9 +- tests/components/recorder/test_purge.py | 108 +++++++++++++++++- 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0d6dddfa2d5..c16d7a2d198 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -306,7 +306,7 @@ def _async_register_services(hass, instance): class PurgeTask(NamedTuple): """Object to store information about purge task.""" - keep_days: int + purge_before: datetime repack: bool apply_filter: bool @@ -451,7 +451,8 @@ class Recorder(threading.Thread): repack = kwargs.get(ATTR_REPACK) apply_filter = kwargs.get(ATTR_APPLY_FILTER) - self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + purge_before = dt_util.utcnow() - timedelta(days=keep_days) + self.queue.put(PurgeTask(purge_before, repack, apply_filter)) def do_adhoc_purge_entities(self, entity_ids, domains, entity_globs): """Trigger an adhoc purge of requested entities.""" @@ -538,7 +539,8 @@ class Recorder(threading.Thread): # Purge will schedule the perodic cleanups # after it completes to ensure it does not happen # until after the database is vacuumed - self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + purge_before = dt_util.utcnow() - timedelta(days=self.keep_days) + self.queue.put(PurgeTask(purge_before, repack=False, apply_filter=False)) else: self.queue.put(PerodicCleanupTask()) @@ -696,16 +698,16 @@ class Recorder(threading.Thread): self.migration_in_progress = False persistent_notification.dismiss(self.hass, "recorder_database_migration") - def _run_purge(self, keep_days, repack, apply_filter): + def _run_purge(self, purge_before, repack, apply_filter): """Purge the database.""" - if purge.purge_old_data(self, keep_days, repack, apply_filter): + if purge.purge_old_data(self, purge_before, repack, apply_filter): # We always need to do the db cleanups after a purge # is finished to ensure the WAL checkpoint and other # tasks happen after a vacuum. perodic_db_cleanups(self) return # Schedule a new purge task if this one didn't finish - self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + self.queue.put(PurgeTask(purge_before, repack, apply_filter)) def _run_purge_entities(self, entity_filter): """Purge entities from the database.""" @@ -724,7 +726,7 @@ class Recorder(threading.Thread): def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): - self._run_purge(event.keep_days, event.repack, event.apply_filter) + self._run_purge(event.purge_before, event.repack, event.apply_filter) return if isinstance(event, PurgeEntitiesTask): self._run_purge_entities(event.entity_filter) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index e1cf15e331d..49803117119 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -1,15 +1,13 @@ """Purge old data helper.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime import logging from typing import TYPE_CHECKING, Callable from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct -import homeassistant.util.dt as dt_util - from .const import MAX_ROWS_TO_PURGE from .models import Events, RecorderRuns, States from .repack import repack_database @@ -23,13 +21,12 @@ _LOGGER = logging.getLogger(__name__) @retryable_database_job("purge") def purge_old_data( - instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False + instance: Recorder, purge_before: datetime, repack: bool, apply_filter: bool = False ) -> bool: - """Purge events and states older than purge_days ago. + """Purge events and states older than purge_before. Cleans up an timeframe of an hour, based on the oldest record. """ - purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 6727b4da495..40ad71096c1 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -8,6 +8,8 @@ from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session from homeassistant.components import recorder +from homeassistant.components.recorder import PurgeTask +from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE from homeassistant.components.recorder.models import Events, RecorderRuns, States from homeassistant.components.recorder.purge import purge_old_data from homeassistant.components.recorder.util import session_scope @@ -43,8 +45,10 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 6 + purge_before = dt_util.utcnow() - timedelta(days=4) + # run purge_old_data() - finished = purge_old_data(instance, 4, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert not finished assert states.count() == 2 @@ -52,7 +56,7 @@ async def test_purge_old_states( assert states_after_purge[1].old_state_id == states_after_purge[0].state_id assert states_after_purge[0].old_state_id is None - finished = purge_old_data(instance, 4, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert finished assert states.count() == 2 @@ -162,13 +166,15 @@ async def test_purge_old_events( events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == 6 + purge_before = dt_util.utcnow() - timedelta(days=4) + # run purge_old_data() - finished = purge_old_data(instance, 4, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert not finished assert events.count() == 2 # we should only have 2 events left - finished = purge_old_data(instance, 4, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert finished assert events.count() == 2 @@ -186,11 +192,13 @@ async def test_purge_old_recorder_runs( recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 7 + purge_before = dt_util.utcnow() + # run purge_old_data() - finished = purge_old_data(instance, 0, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert not finished - finished = purge_old_data(instance, 0, repack=False) + finished = purge_old_data(instance, purge_before, repack=False) assert finished assert recorder_runs.count() == 1 @@ -322,6 +330,94 @@ async def test_purge_edge_case( assert events.count() == 0 +async def test_purge_cutoff_date( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test states and events are purged only if they occurred before "now() - keep_days".""" + + async def _add_db_entries(hass: HomeAssistant, cutoff: datetime, rows: int) -> None: + timestamp_keep = cutoff + timestamp_purge = cutoff - timedelta(microseconds=1) + + with recorder.session_scope(hass=hass) as session: + session.add( + Events( + event_id=1000, + event_type="KEEP", + event_data="{}", + origin="LOCAL", + created=timestamp_keep, + time_fired=timestamp_keep, + ) + ) + session.add( + States( + entity_id="test.cutoff", + domain="sensor", + state="keep", + attributes="{}", + last_changed=timestamp_keep, + last_updated=timestamp_keep, + created=timestamp_keep, + event_id=1000, + ) + ) + for row in range(1, rows): + session.add( + Events( + event_id=1000 + row, + event_type="PURGE", + event_data="{}", + origin="LOCAL", + created=timestamp_purge, + time_fired=timestamp_purge, + ) + ) + session.add( + States( + entity_id="test.cutoff", + domain="sensor", + state="purge", + attributes="{}", + last_changed=timestamp_purge, + last_updated=timestamp_purge, + created=timestamp_purge, + event_id=1000 + row, + ) + ) + + instance = await async_setup_recorder_instance(hass, None) + await async_wait_purge_done(hass, instance) + + service_data = {"keep_days": 2} + + # Force multiple purge batches to be run + rows = MAX_ROWS_TO_PURGE + 1 + cutoff = dt_util.utcnow() - timedelta(days=service_data["keep_days"]) + await _add_db_entries(hass, cutoff, rows) + + with session_scope(hass=hass) as session: + states = session.query(States) + events = session.query(Events) + assert states.filter(States.state == "purge").count() == rows - 1 + assert states.filter(States.state == "keep").count() == 1 + assert events.filter(Events.event_type == "PURGE").count() == rows - 1 + assert events.filter(Events.event_type == "KEEP").count() == 1 + + instance.queue.put(PurgeTask(cutoff, repack=False, apply_filter=False)) + await hass.async_block_till_done() + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + + states = session.query(States) + events = session.query(Events) + assert states.filter(States.state == "purge").count() == 0 + assert states.filter(States.state == "keep").count() == 1 + assert events.filter(Events.event_type == "PURGE").count() == 0 + assert events.filter(Events.event_type == "KEEP").count() == 1 + + async def test_purge_filtered_states( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, From 4abdeec36ddcf7d69c855df50bec375b4efc3bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 25 Jun 2021 23:31:17 +0200 Subject: [PATCH 536/750] Use entity class vars in Broadlink (#52177) --- homeassistant/components/broadlink/remote.py | 36 +++------- homeassistant/components/broadlink/sensor.py | 40 +++-------- homeassistant/components/broadlink/switch.py | 75 ++++---------------- 3 files changed, 32 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 3e0c37d3f55..b9dd34d22d8 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -125,28 +125,12 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self._storage_loaded = False self._codes = {} self._flags = defaultdict(int) - self._state = True self._lock = asyncio.Lock() - @property - def name(self): - """Return the name of the remote.""" - return f"{self._device.name} Remote" - - @property - def unique_id(self): - """Return the unique id of the remote.""" - return self._device.unique_id - - @property - def is_on(self): - """Return True if the remote is on.""" - return self._state - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND + self._attr_name = f"{self._device.name} Remote" + self._attr_is_on = True + self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND + self._attr_unique_id = self._device.unique_id def _extract_codes(self, commands, device=None): """Extract a list of codes. @@ -204,7 +188,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): async def async_added_to_hass(self): """Call when the remote is added to hass.""" state = await self.async_get_last_state() - self._state = state is None or state.state != STATE_OFF + self._attr_is_on = state is None or state.state != STATE_OFF self.async_on_remove( self._coordinator.async_add_listener(self.async_write_ha_state) @@ -216,12 +200,12 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Turn on the remote.""" - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off the remote.""" - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def _async_load_storage(self): @@ -242,7 +226,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): delay = kwargs[ATTR_DELAY_SECS] service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}" - if not self._state: + if not self._attr_is_on: _LOGGER.warning( "%s canceled: %s entity is turned off", service, self.entity_id ) @@ -297,7 +281,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): toggle = kwargs[ATTR_ALTERNATIVE] service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}" - if not self._state: + if not self._attr_is_on: _LOGGER.warning( "%s canceled: %s entity is turned off", service, self.entity_id ) @@ -455,7 +439,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): device = kwargs[ATTR_DEVICE] service = f"{RM_DOMAIN}.{SERVICE_DELETE_COMMAND}" - if not self._state: + if not self._attr_is_on: _LOGGER.warning( "%s canceled: %s entity is turned off", service, diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 15445699a33..851668fdeff 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -76,43 +76,21 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity): super().__init__(device) self._coordinator = device.update_manager.coordinator self._monitored_condition = monitored_condition - self._state = self._coordinator.data[monitored_condition] - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return f"{self._device.unique_id}-{self._monitored_condition}" - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" - return SENSOR_TYPES[self._monitored_condition][1] - - @property - def device_class(self): - """Return device class.""" - return SENSOR_TYPES[self._monitored_condition][2] - - @property - def state_class(self): - """Return state class.""" - return SENSOR_TYPES[self._monitored_condition][3] + self._attr_device_class = SENSOR_TYPES[self._monitored_condition][2] + self._attr_name = ( + f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}" + ) + self._attr_state_class = SENSOR_TYPES[self._monitored_condition][3] + self._attr_state = self._coordinator.data[monitored_condition] + self._attr_unique_id = f"{self._device.unique_id}-{self._monitored_condition}" + self._attr_unit_of_measurement = SENSOR_TYPES[self._monitored_condition][1] @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: - self._state = self._coordinator.data[self._monitored_condition] + self._attr_state = self._coordinator.data[self._monitored_condition] self.async_write_ha_state() async def async_added_to_hass(self): diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 4aed6e2288a..1f599d6d108 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -143,26 +143,16 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._coordinator = device.update_manager.coordinator self._state = None - @property - def name(self): - """Return the name of the switch.""" - return f"{self._device.name} Switch" - - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return True + self._attr_assumed_state = True + self._attr_device_class = DEVICE_CLASS_SWITCH + self._attr_name = f"{self._device.name} Switch" + self._attr_unique_id = self._device.unique_id @property def is_on(self): """Return True if the switch is on.""" return self._state - @property - def device_class(self): - """Return device class.""" - return DEVICE_CLASS_SWITCH - @callback def update_data(self): """Update data.""" @@ -204,12 +194,7 @@ class BroadlinkRMSwitch(BroadlinkSwitch): super().__init__( device, config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_OFF) ) - self._name = config[CONF_NAME] - - @property - def name(self): - """Return the name of the switch.""" - return self._name + self._attr_name = config[CONF_NAME] async def _async_send_packet(self, packet): """Send a packet to the device.""" @@ -231,11 +216,6 @@ class BroadlinkSP1Switch(BroadlinkSwitch): """Initialize the switch.""" super().__init__(device, 1, 0) - @property - def unique_id(self): - """Return the unique id of the switch.""" - return self._device.unique_id - async def _async_send_packet(self, packet): """Send a packet to the device.""" try: @@ -255,10 +235,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): self._state = self._coordinator.data["pwr"] self._load_power = self._coordinator.data.get("power") - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return False + self._attr_assumed_state = False @property def current_power_w(self): @@ -283,20 +260,9 @@ class BroadlinkMP1Slot(BroadlinkSwitch): self._slot = slot self._state = self._coordinator.data[f"s{slot}"] - @property - def unique_id(self): - """Return the unique id of the slot.""" - return f"{self._device.unique_id}-s{self._slot}" - - @property - def name(self): - """Return the name of the switch.""" - return f"{self._device.name} S{self._slot}" - - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return False + self._attr_name = f"{self._device.name} S{self._slot}" + self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}" + self._attr_assumed_state = False @callback def update_data(self): @@ -326,25 +292,10 @@ class BroadlinkBG1Slot(BroadlinkSwitch): self._slot = slot self._state = self._coordinator.data[f"pwr{slot}"] - @property - def unique_id(self): - """Return the unique id of the slot.""" - return f"{self._device.unique_id}-s{self._slot}" - - @property - def name(self): - """Return the name of the switch.""" - return f"{self._device.name} S{self._slot}" - - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return False - - @property - def device_class(self): - """Return device class.""" - return DEVICE_CLASS_OUTLET + self._attr_name = f"{self._device.name} S{self._slot}" + self._attr_device_class = DEVICE_CLASS_OUTLET + self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}" + self._attr_assumed_state = False @callback def update_data(self): From 8d8af60b1d0c63e4befad24f5d3af7dd126c3bb0 Mon Sep 17 00:00:00 2001 From: Appleguru Date: Fri, 25 Jun 2021 17:49:35 -0400 Subject: [PATCH 537/750] Add retries for tplink discovery (#52015) * Add retries for tplink discovery * Black Format tplink common.py * Exit tplink discovery early if all devices found * Fix typo in tplink retry log msg * Code style cleanup for tplink retries * Update homeassistant/components/tplink/common.py Co-authored-by: Teemu R. * Fix linting errors for tplink retries Co-authored-by: Teemu R. --- homeassistant/components/tplink/__init__.py | 9 +++++- homeassistant/components/tplink/common.py | 33 ++++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index fe00edd24b8..1f843d364d8 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -76,6 +77,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" config_data = hass.data[DOMAIN].get(ATTR_CONFIG) + device_registry = dr.async_get(hass) + tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + device_count = len(tplink_devices) + # These will contain the initialized devices lights = hass.data[DOMAIN][CONF_LIGHT] = [] switches = hass.data[DOMAIN][CONF_SWITCH] = [] @@ -90,7 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Add discovered devices if config_data is None or config_data[CONF_DISCOVERY]: - discovered_devices = await async_discover_devices(hass, static_devices) + discovered_devices = await async_discover_devices( + hass, static_devices, device_count + ) lights.extend(discovered_devices.lights) switches.extend(discovered_devices.switches) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 8b1ee4a44b1..3096281776a 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -26,6 +26,7 @@ CONF_DISCOVERY = "discovery" CONF_LIGHT = "light" CONF_STRIP = "strip" CONF_SWITCH = "switch" +MAX_DISCOVERY_RETRIES = 4 class SmartDevices: @@ -67,12 +68,9 @@ async def async_get_discoverable_devices(hass: HomeAssistant) -> dict[str, Smart async def async_discover_devices( - hass: HomeAssistant, existing_devices: SmartDevices + hass: HomeAssistant, existing_devices: SmartDevices, target_device_count: int ) -> SmartDevices: """Get devices through discovery.""" - _LOGGER.debug("Discovering devices") - devices = await async_get_discoverable_devices(hass) - _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devices)) lights = [] switches = [] @@ -100,6 +98,33 @@ async def async_discover_devices( else: _LOGGER.error("Unknown smart device type: %s", type(dev)) + devices = {} + for attempt in range(1, MAX_DISCOVERY_RETRIES + 1): + _LOGGER.debug( + "Discovering tplink devices, attempt %s of %s", + attempt, + MAX_DISCOVERY_RETRIES, + ) + discovered_devices = await async_get_discoverable_devices(hass) + _LOGGER.info( + "Discovered %s TP-Link of expected %s smart home device(s)", + len(discovered_devices), + target_device_count, + ) + for device_ip in discovered_devices: + devices[device_ip] = discovered_devices[device_ip] + + if len(discovered_devices) >= target_device_count: + _LOGGER.info( + "Discovered at least as many devices on the network as exist in our device registry, no need to retry" + ) + break + + _LOGGER.info( + "Found %s unique TP-Link smart home device(s) after %s discovery attempts", + len(devices), + attempt, + ) await hass.async_add_executor_job(process_devices) return SmartDevices(lights, switches) From cd9fa27f2a725d7f1f1c2e57f975a37e4aaefaca Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 26 Jun 2021 00:10:53 +0000 Subject: [PATCH 538/750] [ci skip] Translation update --- .../demo/translations/select.it.json | 9 ++++ .../components/dsmr/translations/it.json | 36 ++++++++++++- .../components/dsmr/translations/nl.json | 1 + .../components/dsmr/translations/no.json | 38 +++++++++++++- .../components/dsmr/translations/zh-Hant.json | 38 +++++++++++++- .../pvpc_hourly_pricing/translations/it.json | 21 ++++++-- .../components/select/translations/it.json | 14 ++++++ .../simplisafe/translations/it.json | 2 +- .../components/zwave_js/translations/ca.json | 50 +++++++++++++++++++ .../components/zwave_js/translations/it.json | 50 +++++++++++++++++++ .../components/zwave_js/translations/nl.json | 49 ++++++++++++++++++ .../components/zwave_js/translations/no.json | 50 +++++++++++++++++++ .../components/zwave_js/translations/ru.json | 43 +++++++++++++++- .../zwave_js/translations/zh-Hant.json | 50 +++++++++++++++++++ 14 files changed, 441 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.it.json create mode 100644 homeassistant/components/select/translations/it.json diff --git a/homeassistant/components/demo/translations/select.it.json b/homeassistant/components/demo/translations/select.it.json new file mode 100644 index 00000000000..ba49e1ab60a --- /dev/null +++ b/homeassistant/components/demo/translations/select.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocit\u00e0 della luce", + "ludicrous_speed": "Velocit\u00e0 comica", + "ridiculous_speed": "Velocit\u00e0 ridicola" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index f841ed32984..e1287c4af39 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -1,15 +1,47 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_communicate": "Impossibile comunicare", + "cannot_connect": "Impossibile connettersi" }, "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_communicate": "Impossibile comunicare", + "cannot_connect": "Impossibile connettersi", "one": "Pi\u00f9", "other": "Altri" }, "step": { "one": "Pi\u00f9", - "other": "Altri" + "other": "Altri", + "setup_network": { + "data": { + "dsmr_version": "Seleziona la versione DSMR", + "host": "Host", + "port": "Porta" + }, + "title": "Seleziona l'indirizzo di connessione" + }, + "setup_serial": { + "data": { + "dsmr_version": "Seleziona la versione DSMR", + "port": "Seleziona il dispositivo" + }, + "title": "Dispositivo" + }, + "setup_serial_manual_path": { + "data": { + "port": "Percorso del dispositivo USB" + }, + "title": "Percorso" + }, + "user": { + "data": { + "type": "Tipo di connessione" + }, + "title": "Selezionare il tipo di connessione" + } } }, "options": { diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json index 8827bed5fee..49add2c0e13 100644 --- a/homeassistant/components/dsmr/translations/nl.json +++ b/homeassistant/components/dsmr/translations/nl.json @@ -6,6 +6,7 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", + "cannot_communicate": "Kon niet verbinden.", "cannot_connect": "Kan geen verbinding maken", "one": "Leeg", "other": "Leeg" diff --git a/homeassistant/components/dsmr/translations/no.json b/homeassistant/components/dsmr/translations/no.json index e51520bf730..ef9d2798f2b 100644 --- a/homeassistant/components/dsmr/translations/no.json +++ b/homeassistant/components/dsmr/translations/no.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "cannot_communicate": "Kunne ikke kommunisere", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_communicate": "Kunne ikke kommunisere", + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "Velg DSMR-versjon", + "host": "Vert", + "port": "Port" + }, + "title": "Velg tilkoblingsadresse" + }, + "setup_serial": { + "data": { + "dsmr_version": "Velg DSMR-versjon", + "port": "Velg enhet" + }, + "title": "Enhet" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB enhetsbane" + }, + "title": "Bane" + }, + "user": { + "data": { + "type": "Tilkoblingstype" + }, + "title": "Velg tilkoblingstype" + } } }, "options": { diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index 52e77cd3520..9d95685a87f 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_communicate": "\u901a\u8a0a\u5931\u6557", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_communicate": "\u901a\u8a0a\u5931\u6557", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "\u9078\u64c7 DSM \u7248\u672c", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u9078\u64c7\u9023\u7dda\u4f4d\u5740" + }, + "setup_serial": { + "data": { + "dsmr_version": "\u9078\u64c7 DSM \u7248\u672c", + "port": "\u9078\u64c7\u88dd\u7f6e" + }, + "title": "\u88dd\u7f6e" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "title": "\u8def\u5f91" + }, + "user": { + "data": { + "type": "\u9023\u7dda\u985e\u578b" + }, + "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + } } }, "options": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json index e36fc746883..79e6e627a7e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json @@ -7,10 +7,25 @@ "user": { "data": { "name": "Nome del sensore", - "tariff": "Tariffa contrattuale (1, 2 o 3 periodi)" + "power": "Potenza contrattuale (kW)", + "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", + "tariff": "Tariffa applicabile per zona geografica" }, - "description": "Questo sensore utilizza l'API ufficiale per ottenere [prezzi orari dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visitare la [documentazione di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelezionare la tariffa contrattuale in base al numero di periodi di fatturazione al giorno:\n- 1 periodo: normale\n- 2 periodi: discriminazione (tariffa notturna)\n- 3 periodi: auto elettrica (tariffa notturna di 3 periodi)", - "title": "Selezione della tariffa" + "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configurazione del sensore" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Potenza contrattuale (kW)", + "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", + "tariff": "Tariffa applicabile per zona geografica" + }, + "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configurazione del sensore" } } } diff --git a/homeassistant/components/select/translations/it.json b/homeassistant/components/select/translations/it.json new file mode 100644 index 00000000000..06a3f47ce7d --- /dev/null +++ b/homeassistant/components/select/translations/it.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Cambia l'opzione {entity_name}" + }, + "condition_type": { + "selected_option": "Opzione selezionata {entity_name} corrente" + }, + "trigger_type": { + "current_option_changed": "Opzione {entity_name} modificata" + } + }, + "title": "Selezionare" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index b5ce2a26702..13e7fe4562a 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Il token di accesso \u00e8 scaduto o \u00e8 stato revocato. Inserisci la tua password per ricollegare il tuo account.", + "description": "L'accesso \u00e8 scaduto o revocato. Inserisci la password per ri-collegare il tuo account.", "title": "Autenticare nuovamente l'integrazione" }, "user": { diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index af39281c06b..d487ea8b902 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", + "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.", + "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", + "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", + "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "different_device": "El dispositiu USB connectat per a aquesta entrada de configuraci\u00f3 no \u00e9s el mateix que el configurat anteriorment. Crea na entrada de configuraci\u00f3 nova per al dispositiu nou." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_ws_url": "URL websocket inv\u00e0lid", + "unknown": "Error inesperat" + }, + "progress": { + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts.", + "start_addon": "Espera mentre es completa la inicialitzaci\u00f3 del complement Z-Wave JS. Pot tardar uns segons." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emula maquinari", + "log_level": "Nivell dels registres", + "network_key": "Clau de xarxa", + "usb_path": "Ruta del port USB del dispositiu" + }, + "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" + }, + "install_addon": { + "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Utilitza el complement Z-Wave JS Supervisor" + }, + "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?", + "title": "Selecciona el m\u00e8tode de connexi\u00f3" + }, + "start_addon": { + "title": "El complement Z-Wave JS s'est\u00e0 iniciant." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index f3005fc1651..71832e4882b 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", + "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.", + "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", + "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", + "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "different_device": "Il dispositivo USB connesso non \u00e8 lo stesso configurato in precedenza per questa voce di configurazione. Si prega, invece, di creare una nuova voce di configurazione per il nuovo dispositivo." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_ws_url": "URL del websocket non valido", + "unknown": "Errore imprevisto" + }, + "progress": { + "install_addon": "Attendere il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questo pu\u00f2 richiedere diversi minuti.", + "start_addon": "Attendere il completamento dell'avvio del componente aggiuntivo Z-Wave JS. Questa operazione potrebbe richiedere alcuni secondi." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulare l'hardware", + "log_level": "Livello di registro", + "network_key": "Chiave di rete", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "Entra nella configurazione del componente aggiuntivo Z-Wave JS" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo Z-Wave JS \u00e8 iniziata" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Usa il componente aggiuntivo Z-Wave JS di Supervisor" + }, + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS di Supervisor?", + "title": "Seleziona il metodo di connessione" + }, + "start_addon": { + "title": "Il componente aggiuntivo Z-Wave JS si sta avviando." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 090733da15b..1e37b617c6d 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -51,5 +51,54 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", + "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.", + "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", + "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", + "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_ws_url": "Ongeldige websocket URL", + "unknown": "Onverwachte fout" + }, + "progress": { + "install_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on installatie voltooid is. Dit kan enkele minuten duren.", + "start_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on start voltooid is. Dit kan enkele seconden duren." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulate Hardware", + "log_level": "Log level", + "network_key": "Netwerksleutel", + "usb_path": "USB-apparaatpad" + }, + "title": "Voer de configuratie van de Z-Wave JS-add-on in" + }, + "install_addon": { + "title": "De Z-Wave JS add-on installatie is gestart" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Gebruik de Z-Wave JS Supervisor add-on" + }, + "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?", + "title": "Selecteer een verbindingsmethode" + }, + "start_addon": { + "title": "The Z-Wave JS add-on is aan het starten." + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index aa0fa2451aa..8eb4c176356 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", + "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg", + "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", + "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", + "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.", + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "different_device": "Den tilkoblede USB-enheten er ikke den samme som tidligere konfigurert for denne konfigurasjonsoppf\u00f8ringen. Opprett i stedet en ny konfigurasjonsoppf\u00f8ring for den nye enheten." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_ws_url": "Ugyldig URL-adresse for websocket", + "unknown": "Uventet feil" + }, + "progress": { + "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter.", + "start_addon": "Vent mens Z-Wave JS-tillegget er ferdig startet. Dette kan ta noen sekunder." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emuler maskinvare", + "log_level": "Loggniv\u00e5", + "network_key": "Nettverksn\u00f8kkel", + "usb_path": "USB enhetsbane" + }, + "title": "Angi konfigurasjon for Z-Wave JS-tillegg" + }, + "install_addon": { + "title": "Installasjon av Z-Wave JS-tillegg har startet" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Bruk Z-Wave JS Supervisor-tillegg" + }, + "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?", + "title": "Velg tilkoblingsmetode" + }, + "start_addon": { + "title": "Z-Wave JS-tillegget starter" + } + } + }, "title": "" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 1655a84190d..64c8101740b 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -41,7 +41,7 @@ }, "on_supervisor": { "data": { - "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS" + "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS" }, "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" @@ -59,7 +59,46 @@ "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.", "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "different_device": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0435 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u0440\u0430\u043d\u0435\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043d\u043e\u0432\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\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.", + "invalid_ws_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "progress": { + "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442.", + "start_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "\u042d\u043c\u0443\u043b\u044f\u0446\u0438\u044f \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f", + "log_level": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u0430", + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, + "install_addon": { + "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS" + }, + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "start_addon": { + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f" + } } }, "title": "Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 827c0b54e90..3e43c500c39 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", + "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", + "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "different_device": "\u6240\u9023\u63a5\u7684 USB \u88dd\u7f6e\u8207\u4e4b\u524d\u7684\u8a2d\u5b9a\u88dd\u7f6e\u4e0d\u540c\uff0c\u8acb\u91dd\u5c0d\u65b0\u88dd\u7f6e\u65b0\u589e\u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_ws_url": "Websocket URL \u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "progress": { + "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002", + "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "\u6a21\u64ec\u786c\u9ad4", + "log_level": "\u65e5\u8a8c\u8a18\u9304\u7b49\u7d1a", + "network_key": "\u7db2\u8def\u5bc6\u9470", + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" + }, + "install_addon": { + "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5" + }, + "manual": { + "data": { + "url": "\u7db2\u5740" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" + }, + "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", + "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + }, + "start_addon": { + "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file From 568e1b379dd89630c4a5d24d02ae62caee740a47 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 26 Jun 2021 08:30:47 +0300 Subject: [PATCH 539/750] Address late review of Switcher sensor migration (#52186) --- .../components/switcher_kis/sensor.py | 10 +++++----- .../components/switcher_kis/switch.py | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 037e7297cee..5b6b40a0e2d 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ELECTRICAL_CURRENT_AMPERE, POWER_WATT -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType, StateType @@ -124,8 +124,8 @@ class SwitcherSensorEntity(SensorEntity): ) ) - async def async_update_data(self, device_data: SwitcherV2Device) -> None: + @callback + def async_update_data(self, device_data: SwitcherV2Device) -> None: """Update the entity data.""" - if device_data: - self._device_data = device_data - self.async_write_ha_state() + self._device_data = device_data + self.async_write_ha_state() diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 27d9e16e1f8..21ebcf54cc7 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -13,7 +13,7 @@ from aioswitcher.devices import SwitcherV2Device import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -135,15 +135,15 @@ class SwitcherControl(SwitchEntity): ) ) - async def async_update_data(self, device_data: SwitcherV2Device) -> None: + @callback + def async_update_data(self, device_data: SwitcherV2Device) -> None: """Update the entity data.""" - if device_data: - if self._self_initiated: - self._self_initiated = False - else: - self._device_data = device_data - self._state = self._device_data.state - self.async_write_ha_state() + if self._self_initiated: + self._self_initiated = False + else: + self._device_data = device_data + self._state = self._device_data.state + self.async_write_ha_state() async def async_turn_on(self, **kwargs: dict) -> None: """Turn the entity on.""" From b7c15d447441fe29e8f2abbee91cce8a48367aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Jun 2021 13:12:10 +0200 Subject: [PATCH 540/750] Fix deprecation warning in discord notifier (#52197) --- homeassistant/components/discord/notify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index dfc89a4cb7e..6d3dc704d83 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -112,7 +112,6 @@ class DiscordNotificationService(BaseNotificationService): await channel.send(message, files=files) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) - await discord_bot.logout() await discord_bot.close() # Using reconnect=False prevents multiple ready events to be fired. From 5687ced7b3c42131a901c12d6a1d94a88af37f41 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 26 Jun 2021 14:30:36 +0200 Subject: [PATCH 541/750] Cleanup KNX integration (#52168) Co-authored-by: Franck Nijhof --- homeassistant/components/knx/__init__.py | 20 ++++----- homeassistant/components/knx/binary_sensor.py | 31 ++++--------- homeassistant/components/knx/climate.py | 32 ++++---------- homeassistant/components/knx/const.py | 2 +- homeassistant/components/knx/cover.py | 32 +++++--------- homeassistant/components/knx/fan.py | 28 +++++------- homeassistant/components/knx/knx_entity.py | 13 +----- homeassistant/components/knx/light.py | 37 ++++++---------- homeassistant/components/knx/number.py | 17 ++++--- homeassistant/components/knx/scene.py | 14 +++--- homeassistant/components/knx/select.py | 21 +++++---- homeassistant/components/knx/sensor.py | 44 ++++++------------- homeassistant/components/knx/switch.py | 14 +++--- homeassistant/components/knx/weather.py | 20 +++------ 14 files changed, 115 insertions(+), 210 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7b6ccf31b17..b56331bc80c 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -298,7 +298,6 @@ class KNXModule: return self.connection_config_tunneling() if CONF_KNX_ROUTING in self.config[DOMAIN]: return self.connection_config_routing() - # config from xknx.yaml always has priority later on return ConnectionConfig(auto_reconnect=True) def connection_config_routing(self) -> ConnectionConfig: @@ -379,14 +378,16 @@ class KNXModule: "Service event_register could not remove event for '%s'", str(group_address), ) - else: - for group_address in group_addresses: - if group_address not in self._knx_event_callback.group_addresses: - self._knx_event_callback.group_addresses.append(group_address) - _LOGGER.debug( - "Service event_register registered event for '%s'", - str(group_address), - ) + return + + for group_address in group_addresses: + if group_address in self._knx_event_callback.group_addresses: + continue + self._knx_event_callback.group_addresses.append(group_address) + _LOGGER.debug( + "Service event_register registered event for '%s'", + str(group_address), + ) async def service_exposure_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing an exposure to KNX bus.""" @@ -405,7 +406,6 @@ class KNXModule: if group_address in self.service_exposures: replaced_exposure = self.service_exposures.pop(group_address) - assert replaced_exposure.device is not None _LOGGER.warning( "Service exposure_register replacing already registered exposure for '%s' - %s", group_address, diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index a3271e605af..9a9c9627670 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -31,19 +31,18 @@ async def async_setup_platform( platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXBinarySensor(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXBinarySensor(xknx, entity_config) for entity_config in platform_config + ) class KNXBinarySensor(KnxEntity, BinarySensorEntity): """Representation of a KNX binary sensor.""" + _device: XknxBinarySensor + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX binary sensor.""" - self._device: XknxBinarySensor super().__init__( device=XknxBinarySensor( xknx, @@ -58,13 +57,9 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), ) ) - self._device_class: str | None = config.get(CONF_DEVICE_CLASS) - self._unique_id = f"{self._device.remote_value.group_address_state}" - - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return self._device_class + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_force_update = self._device.ignore_internal_state + self._attr_unique_id = str(self._device.remote_value.group_address_state) @property def is_on(self) -> bool: @@ -84,13 +79,3 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): dt.as_utc(self._device.last_telegram.timestamp) ) return attr - - @property - def force_update(self) -> bool: - """ - Return True if state updates should be forced. - - If True, a state change will be triggered anytime the state property is - updated, not just when the value changes. - """ - return self._device.ignore_internal_state diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index adf585555fd..b43f0efe7f0 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -46,11 +46,9 @@ async def async_setup_platform( xknx: XKNX = hass.data[DOMAIN].xknx _async_migrate_unique_id(hass, platform_config) - entities = [] - for entity_config in platform_config: - entities.append(KNXClimate(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXClimate(xknx, entity_config) for entity_config in platform_config + ) @callback @@ -168,22 +166,20 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" + _device: XknxClimate + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" - self._device: XknxClimate super().__init__(_create_climate(xknx, config)) - self._unique_id = ( + self._attr_target_temperature_step = self._device.temperature_step + self._attr_unique_id = ( f"{self._device.temperature.group_address_state}_" f"{self._device.target_temperature.group_address_state}_" f"{self._device.target_temperature.group_address}_" f"{self._device._setpoint_shift.group_address}" # pylint: disable=protected-access ) - self._unit_of_measurement = TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE async def async_update(self) -> None: """Request a state update from KNX bus.""" @@ -191,21 +187,11 @@ class KNXClimate(KnxEntity, ClimateEntity): if self._device.mode is not None: await self._device.mode.sync() - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return self._unit_of_measurement - @property def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.temperature.value - @property - def target_temperature_step(self) -> float: - """Return the supported step of target temperature.""" - return self._device.temperature_step - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index b1cf80332a7..421297da9d6 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -37,8 +37,8 @@ CONF_STATE_ADDRESS: Final = "state_address" CONF_SYNC_STATE: Final = "sync_state" ATTR_COUNTER: Final = "counter" -ATTR_SOURCE: Final = "source" ATTR_LAST_KNX_UPDATE: Final = "last_knx_update" +ATTR_SOURCE: Final = "source" class ColorTempModes(Enum): diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 58d627ecb5e..92edf804bc6 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -44,15 +44,12 @@ async def async_setup_platform( if not discovery_info or not discovery_info["platform_config"]: return platform_config = discovery_info["platform_config"] - _async_migrate_unique_id(hass, platform_config) - xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXCover(xknx, entity_config)) - - async_add_entities(entities) + _async_migrate_unique_id(hass, platform_config) + async_add_entities( + KNXCover(xknx, entity_config) for entity_config in platform_config + ) @callback @@ -85,9 +82,10 @@ def _async_migrate_unique_id( class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" + _device: XknxCover + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize the cover.""" - self._device: XknxCover super().__init__( device=XknxCover( xknx, @@ -109,12 +107,15 @@ class KNXCover(KnxEntity, CoverEntity): invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], ) ) - self._device_class: str | None = config.get(CONF_DEVICE_CLASS) - self._unique_id = ( + self._unsubscribe_auto_updater: Callable[[], None] | None = None + + self._attr_device_class = config.get(CONF_DEVICE_CLASS) or ( + DEVICE_CLASS_BLIND if self._device.supports_angle else None + ) + self._attr_unique_id = ( f"{self._device.updown.group_address}_" f"{self._device.position_target.group_address}" ) - self._unsubscribe_auto_updater: Callable[[], None] | None = None @callback async def after_update_callback(self, device: XknxDevice) -> None: @@ -123,15 +124,6 @@ class KNXCover(KnxEntity, CoverEntity): if self._device.is_traveling(): self.start_auto_updater() - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - if self._device_class: - return self._device_class - if self._device.supports_angle: - return DEVICE_CLASS_BLIND - return None - @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index b3c19d2cfd6..5d66eb1ceb6 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -34,23 +34,19 @@ async def async_setup_platform( """Set up fans for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXFan(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities(KNXFan(xknx, entity_config) for entity_config in platform_config) class KNXFan(KnxEntity, FanEntity): """Representation of a KNX fan.""" + _device: XknxFan + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX fan.""" - self._device: XknxFan max_step = config.get(FanSchema.CONF_MAX_STEP) super().__init__( device=XknxFan( @@ -67,10 +63,16 @@ class KNXFan(KnxEntity, FanEntity): max_step=max_step, ) ) - self._unique_id = f"{self._device.speed.group_address}" # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None + self._attr_supported_features = ( + SUPPORT_SET_SPEED | SUPPORT_OSCILLATE + if self._device.supports_oscillation + else SUPPORT_SET_SPEED + ) + self._attr_unique_id = str(self._device.speed.group_address) + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" if self._step_range: @@ -79,16 +81,6 @@ class KNXFan(KnxEntity, FanEntity): else: await self._device.set_speed(percentage) - @property - def supported_features(self) -> int: - """Flag supported features.""" - flags = SUPPORT_SET_SPEED - - if self._device.supports_oscillation: - flags |= SUPPORT_OSCILLATE - - return flags - @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index a85fb2a561f..5f2e14d1466 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -14,10 +14,11 @@ from .const import DOMAIN class KnxEntity(Entity): """Representation of a KNX entity.""" + _attr_should_poll = False + def __init__(self, device: XknxDevice) -> None: """Set up device.""" self._device = device - self._unique_id: str | None = None @property def name(self) -> str: @@ -30,16 +31,6 @@ class KnxEntity(Entity): knx_module = cast(KNXModule, self.hass.data[DOMAIN]) return knx_module.connected - @property - def should_poll(self) -> bool: - """No polling needed within KNX.""" - return False - - @property - def unique_id(self) -> str | None: - """Return the unique id of the device.""" - return self._unique_id - async def async_update(self) -> None: """Request a state update from KNX bus.""" await self._device.sync() diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index d8697a26d7f..56068b5deae 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -43,15 +43,12 @@ async def async_setup_platform( if not discovery_info or not discovery_info["platform_config"]: return platform_config = discovery_info["platform_config"] - _async_migrate_unique_id(hass, platform_config) - xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXLight(xknx, entity_config)) - - async_add_entities(entities) + _async_migrate_unique_id(hass, platform_config) + async_add_entities( + KNXLight(xknx, entity_config) for entity_config in platform_config + ) @callback @@ -223,19 +220,21 @@ def _create_light(xknx: XKNX, config: ConfigType) -> XknxLight: class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" + _device: XknxLight + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX light.""" - self._device: XknxLight super().__init__(_create_light(xknx, config)) - self._unique_id = self._device_unique_id() - self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._max_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] - self._min_mireds = color_util.color_temperature_kelvin_to_mired( - self._max_kelvin - ) - self._max_mireds = color_util.color_temperature_kelvin_to_mired( + self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] + + self._attr_max_mireds = color_util.color_temperature_kelvin_to_mired( self._min_kelvin ) + self._attr_min_mireds = color_util.color_temperature_kelvin_to_mired( + self._max_kelvin + ) + self._attr_unique_id = self._device_unique_id() def _device_unique_id(self) -> str: """Return unique id for this device.""" @@ -311,16 +310,6 @@ class KNXLight(KnxEntity, LightEntity): ) return None - @property - def min_mireds(self) -> int: - """Return the coldest color temp this light supports in mireds.""" - return self._min_mireds - - @property - def max_mireds(self) -> int: - """Return the warmest color temp this light supports in mireds.""" - return self._max_mireds - @property def color_mode(self) -> str | None: """Return the color mode of the light.""" diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index b438551ebda..eb4f21de513 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -27,7 +27,6 @@ async def async_setup_platform( """Set up number entities for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx @@ -51,25 +50,25 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): """Representation of a KNX number.""" + _device: NumericValue + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize a KNX number.""" - self._device: NumericValue super().__init__(_create_numeric_value(xknx, config)) - self._unique_id = f"{self._device.sensor_value.group_address}" - - self._attr_min_value = config.get( - NumberSchema.CONF_MIN, - self._device.sensor_value.dpt_class.value_min, - ) self._attr_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, ) + self._attr_min_value = config.get( + NumberSchema.CONF_MIN, + self._device.sensor_value.dpt_class.value_min, + ) self._attr_step = config.get( NumberSchema.CONF_STEP, self._device.sensor_value.dpt_class.resolution, ) - self._device.sensor_value.value = max(0, self.min_value) + self._attr_unique_id = str(self._device.sensor_value.group_address) + self._device.sensor_value.value = max(0, self._attr_min_value) async def async_added_to_hass(self) -> None: """Restore last state.""" diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index e3b00815424..9bd32c99e41 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -26,23 +26,21 @@ async def async_setup_platform( """Set up the scenes for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXScene(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXScene(xknx, entity_config) for entity_config in platform_config + ) class KNXScene(KnxEntity, Scene): """Representation of a KNX scene.""" + _device: XknxScene + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Init KNX scene.""" - self._device: XknxScene super().__init__( device=XknxScene( xknx, @@ -51,7 +49,7 @@ class KNXScene(KnxEntity, Scene): scene_number=config[SceneSchema.CONF_SCENE_NUMBER], ) ) - self._unique_id = ( + self._attr_unique_id = ( f"{self._device.scene_value.group_address}_{self._device.scene_number}" ) diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index a545cd3f46b..07f74c04e4f 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -28,10 +28,9 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up number entities for KNX platform.""" + """Set up select entities for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx @@ -54,20 +53,20 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue: class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): - """Representation of a KNX number.""" + """Representation of a KNX select.""" + + _device: RawValue def __init__(self, xknx: XKNX, config: ConfigType) -> None: - """Initialize a KNX number.""" - self._device: RawValue + """Initialize a KNX select.""" super().__init__(_create_raw_value(xknx, config)) - self._unique_id = f"{self._device.remote_value.group_address}" - self._option_payloads: dict[str, int] = { option[SelectSchema.CONF_OPTION]: option[SelectSchema.CONF_PAYLOAD] for option in config[SelectSchema.CONF_OPTIONS] } self._attr_options = list(self._option_payloads) self._attr_current_option = None + self._attr_unique_id = str(self._device.remote_value.group_address) async def async_added_to_hass(self) -> None: """Restore last state.""" @@ -98,7 +97,7 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - if payload := self._option_payloads.get(option): - await self._device.set(payload) - return - raise ValueError(f"Invalid option for {self.entity_id}: {option}") + payload = self._option_payloads.get(option) + if payload is None: + raise ValueError(f"Invalid option for {self.entity_id}: {option}") + await self._device.set(payload) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 21586faf58c..e095b2aee47 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -27,15 +27,12 @@ async def async_setup_platform( """Set up sensor(s) for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXSensor(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXSensor(xknx, entity_config) for entity_config in platform_config + ) def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: @@ -53,30 +50,25 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor: class KNXSensor(KnxEntity, SensorEntity): """Representation of a KNX sensor.""" + _device: XknxSensor + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - self._device: XknxSensor super().__init__(_create_sensor(xknx, config)) - self._unique_id = f"{self._device.sensor_value.group_address_state}" + self._attr_device_class = ( + self._device.ha_device_class() + if self._device.ha_device_class() in DEVICE_CLASSES + else None + ) + self._attr_force_update = self._device.always_callback + self._attr_unique_id = str(self._device.sensor_value.group_address_state) + self._attr_unit_of_measurement = self._device.unit_of_measurement() @property def state(self) -> StateType: """Return the state of the sensor.""" return self._device.resolve_state() - @property - def unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._device.unit_of_measurement() - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - device_class = self._device.ha_device_class() - if device_class in DEVICE_CLASSES: - return device_class - return None - @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" @@ -88,13 +80,3 @@ class KNXSensor(KnxEntity, SensorEntity): dt.as_utc(self._device.last_telegram.timestamp) ) return attr - - @property - def force_update(self) -> bool: - """ - Return True if state updates should be forced. - - If True, a state change will be triggered anytime the state property is - updated, not just when the value changes. - """ - return self._device.always_callback diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index b30313a047a..bb7e582ce15 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -27,23 +27,21 @@ async def async_setup_platform( """Set up switch(es) for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXSwitch(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXSwitch(xknx, entity_config) for entity_config in platform_config + ) class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): """Representation of a KNX switch.""" + _device: XknxSwitch + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX switch.""" - self._device: XknxSwitch super().__init__( device=XknxSwitch( xknx, @@ -53,7 +51,7 @@ class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): invert=config[SwitchSchema.CONF_INVERT], ) ) - self._unique_id = f"{self._device.switch.group_address}" + self._attr_unique_id = str(self._device.switch.group_address) async def async_added_to_hass(self) -> None: """Restore last state.""" diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index b396142e387..4a55f81ff72 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -24,15 +24,12 @@ async def async_setup_platform( """Set up weather entities for KNX platform.""" if not discovery_info or not discovery_info["platform_config"]: return - platform_config = discovery_info["platform_config"] xknx: XKNX = hass.data[DOMAIN].xknx - entities = [] - for entity_config in platform_config: - entities.append(KNXWeather(xknx, entity_config)) - - async_add_entities(entities) + async_add_entities( + KNXWeather(xknx, entity_config) for entity_config in platform_config + ) def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: @@ -74,22 +71,19 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather: class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" + _device: XknxWeather + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" - self._device: XknxWeather super().__init__(_create_weather(xknx, config)) - self._unique_id = f"{self._device._temperature.group_address_state}" + self._attr_unique_id = str(self._device._temperature.group_address_state) @property def temperature(self) -> float | None: """Return current temperature.""" return self._device.temperature - @property - def temperature_unit(self) -> str: - """Return temperature unit.""" - return TEMP_CELSIUS - @property def pressure(self) -> float | None: """Return current air pressure.""" From c558c77413ac360b276c044ec90d3dbee425ad01 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 26 Jun 2021 20:09:53 +0200 Subject: [PATCH 542/750] Correct keyerror exception. (#52150) --- homeassistant/components/modbus/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 062be57f51c..03f27dd461b 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -40,10 +40,10 @@ def sensor_schema_validator(config): structure = ( f">{DEFAULT_STRUCT_FORMAT[config[CONF_DATA_TYPE]][config[CONF_COUNT]]}" ) - except KeyError: + except KeyError as key: raise vol.Invalid( f"Unable to detect data type for {config[CONF_NAME]} sensor, try a custom type" - ) from KeyError + ) from key else: structure = config.get(CONF_STRUCTURE) From 4c7934de4628d3d61aaa8d829b337120ef5d7768 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 26 Jun 2021 20:36:13 +0200 Subject: [PATCH 543/750] Clean up strings.json (#52202) * Remove empty error in arcam_fmj * Remove empty data in directv * Remove empty error and data in kraken * Remove empty data in roku --- homeassistant/components/arcam_fmj/strings.json | 1 - homeassistant/components/directv/strings.json | 1 - homeassistant/components/kraken/strings.json | 2 -- homeassistant/components/roku/strings.json | 3 +-- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json index 154727baf9f..435a6971d5b 100644 --- a/homeassistant/components/arcam_fmj/strings.json +++ b/homeassistant/components/arcam_fmj/strings.json @@ -5,7 +5,6 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, - "error": {}, "flow_title": "{host}", "step": { "confirm": { diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json index e6c54d0d4aa..4384867dfa4 100644 --- a/homeassistant/components/directv/strings.json +++ b/homeassistant/components/directv/strings.json @@ -3,7 +3,6 @@ "flow_title": "{name}", "step": { "ssdp_confirm": { - "data": {}, "description": "Do you want to set up {name}?" }, "user": { diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json index e94f2129a48..10257793de0 100644 --- a/homeassistant/components/kraken/strings.json +++ b/homeassistant/components/kraken/strings.json @@ -3,10 +3,8 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]" }, - "error": {}, "step": { "user": { - "data": {}, "description": "[%key:common::config_flow::description::confirm_setup%]" } } diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 235cf4ad159..68cbe528e87 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -10,8 +10,7 @@ }, "discovery_confirm": { "title": "Roku", - "description": "Do you want to set up {name}?", - "data": {} + "description": "Do you want to set up {name}?" } }, "error": { From db2fda09b9cfdd30d8a96b636b5a3d375d465f42 Mon Sep 17 00:00:00 2001 From: Pavel Pletenev Date: Sun, 27 Jun 2021 01:36:45 +0300 Subject: [PATCH 544/750] Fix habitica regression (#52097) --- homeassistant/components/habitica/__init__.py | 10 +- homeassistant/components/habitica/const.py | 4 + tests/components/habitica/test_init.py | 111 ++++++++++++++++-- 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index e8846d1f85a..efb82a9f1aa 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( ATTR_ARGS, + ATTR_DATA, ATTR_PATH, CONF_API_USER, DEFAULT_URL, @@ -111,7 +112,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def handle_api_call(call): name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] - api = hass.data[DOMAIN].get(name) + entries = hass.config_entries.async_entries(DOMAIN) + api = None + for entry in entries: + if entry.data[CONF_NAME] == name: + api = hass.data[DOMAIN].get(entry.entry_id) + break if api is None: _LOGGER.error("API_CALL: User '%s' not configured", name) return @@ -126,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: kwargs = call.data.get(ATTR_ARGS, {}) data = await api(**kwargs) hass.bus.async_fire( - EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data} + EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} ) data = hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 02a46334c7a..1379f0a6447 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -7,7 +7,11 @@ CONF_API_USER = "api_user" DEFAULT_URL = "https://habitica.com" DOMAIN = "habitica" +# service constants SERVICE_API_CALL = "api_call" ATTR_PATH = CONF_PATH ATTR_ARGS = "args" + +# event constants EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" +ATTR_DATA = "data" diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 5f7e4b7fbf5..97d4fb092fc 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -1,15 +1,33 @@ -"""Test the habitica init module.""" +"""Test the habitica module.""" +import pytest + from homeassistant.components.habitica.const import ( + ATTR_ARGS, + ATTR_DATA, + ATTR_PATH, DEFAULT_URL, DOMAIN, + EVENT_API_CALL_SUCCESS, SERVICE_API_CALL, ) +from homeassistant.components.habitica.sensor import TASKS_TYPES +from homeassistant.const import ATTR_NAME -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_capture_events + +TEST_API_CALL_ARGS = {"text": "Use API from Home Assistant", "type": "todo"} +TEST_USER_NAME = "test_user" -async def test_entry_setup_unload(hass, aioclient_mock): - """Test integration setup and unload.""" +@pytest.fixture +def capture_api_call_success(hass): + """Capture api_call events.""" + return async_capture_events(hass, EVENT_API_CALL_SUCCESS) + + +@pytest.fixture +def habitica_entry(hass): + """Test entry for the following tests.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="test-api-user", @@ -20,17 +38,96 @@ async def test_entry_setup_unload(hass, aioclient_mock): }, ) entry.add_to_hass(hass) + return entry + +@pytest.fixture +def common_requests(aioclient_mock): + """Register requests for the tests.""" aioclient_mock.get( "https://habitica.com/api/v3/user", - json={"data": {"api_user": "test-api-user", "profile": {"name": "test_user"}}}, + json={ + "data": { + "api_user": "test-api-user", + "profile": {"name": TEST_USER_NAME}, + "stats": { + "class": "test-class", + "con": 1, + "exp": 2, + "gp": 3, + "hp": 4, + "int": 5, + "lvl": 6, + "maxHealth": 7, + "maxMP": 8, + "mp": 9, + "per": 10, + "points": 11, + "str": 12, + "toNextLevel": 13, + }, + } + }, + ) + for n_tasks, task_type in enumerate(TASKS_TYPES.keys(), start=1): + aioclient_mock.get( + f"https://habitica.com/api/v3/tasks/user?type={task_type}", + json={ + "data": [ + {"text": f"this is a mock {task_type} #{task}", "id": f"{task}"} + for task in range(n_tasks) + ] + }, + ) + + aioclient_mock.post( + "https://habitica.com/api/v3/tasks/user", + status=201, + json={"data": TEST_API_CALL_ARGS}, ) - assert await hass.config_entries.async_setup(entry.entry_id) + return aioclient_mock + + +async def test_entry_setup_unload(hass, habitica_entry, common_requests): + """Test integration setup and unload.""" + assert await hass.config_entries.async_setup(habitica_entry.entry_id) await hass.async_block_till_done() assert hass.services.has_service(DOMAIN, SERVICE_API_CALL) - assert await hass.config_entries.async_unload(entry.entry_id) + assert await hass.config_entries.async_unload(habitica_entry.entry_id) + + assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL) + + +async def test_service_call( + hass, habitica_entry, common_requests, capture_api_call_success +): + """Test integration setup, service call and unload.""" + + assert await hass.config_entries.async_setup(habitica_entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_API_CALL) + + assert len(capture_api_call_success) == 0 + + TEST_SERVICE_DATA = { + ATTR_NAME: "test_user", + ATTR_PATH: ["tasks", "user", "post"], + ATTR_ARGS: TEST_API_CALL_ARGS, + } + assert await hass.services.async_call( + DOMAIN, SERVICE_API_CALL, TEST_SERVICE_DATA, blocking=True + ) + + assert len(capture_api_call_success) == 1 + captured_data = capture_api_call_success[0].data + captured_data[ATTR_ARGS] = captured_data[ATTR_DATA] + del captured_data[ATTR_DATA] + assert captured_data == TEST_SERVICE_DATA + + assert await hass.config_entries.async_unload(habitica_entry.entry_id) assert not hass.services.has_service(DOMAIN, SERVICE_API_CALL) From b45c8466b430d43dc7f4b53d891f24e484e21296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 27 Jun 2021 00:40:40 +0200 Subject: [PATCH 545/750] Surepetcare, Use entity class vars and some clean up (#52205) --- CODEOWNERS | 2 +- .../components/surepetcare/binary_sensor.py | 40 ++++---------- .../components/surepetcare/manifest.json | 2 +- .../components/surepetcare/sensor.py | 53 +++++-------------- 4 files changed, 26 insertions(+), 71 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8d22525b34b..592aa5923ba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -480,7 +480,7 @@ homeassistant/components/subaru/* @G-Two homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek -homeassistant/components/surepetcare/* @benleb +homeassistant/components/surepetcare/* @benleb @danielhiversen homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index fd75264ee27..2e0dcf79872 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -41,9 +41,7 @@ async def async_setup_platform( EntityType.FEEDER, EntityType.FELAQUA, ]: - entities.append( - DeviceConnectivity(surepy_entity.id, surepy_entity.type, spc) - ) + entities.append(DeviceConnectivity(surepy_entity.id, spc)) if surepy_entity.type == EntityType.PET: entities.append(Pet(surepy_entity.id, spc)) @@ -56,18 +54,17 @@ async def async_setup_platform( class SurePetcareBinarySensor(BinarySensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" + _attr_should_poll = False + def __init__( self, _id: int, spc: SurePetcareAPI, device_class: str, - sure_type: EntityType, ) -> None: """Initialize a Sure Petcare binary sensor.""" self._id = _id - self._device_class = device_class - self._spc: SurePetcareAPI = spc self._surepy_entity: SurepyEntity = self._spc.states[self._id] @@ -81,26 +78,14 @@ class SurePetcareBinarySensor(BinarySensorEntity): self._name = f"{self._surepy_entity.type.name.capitalize()} {name.capitalize()}" - @property - def should_poll(self) -> bool: - """Return if the entity should use default polling.""" - return False + self._attr_device_class = device_class + self._attr_unique_id = f"{self._surepy_entity.household_id}-{self._id}" @property def name(self) -> str: """Return the name of the device if any.""" return self._name - @property - def device_class(self) -> str: - """Return the device class.""" - return None if not self._device_class else self._device_class - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._surepy_entity.household_id}-{self._id}" - @callback def _async_update(self) -> None: """Get the latest data and update the state.""" @@ -121,7 +106,7 @@ class Hub(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, EntityType.HUB) + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) @property def available(self) -> bool: @@ -153,7 +138,7 @@ class Pet(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, EntityType.PET) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE) @property def is_on(self) -> bool: @@ -186,22 +171,19 @@ class DeviceConnectivity(SurePetcareBinarySensor): def __init__( self, _id: int, - sure_type: EntityType, spc: SurePetcareAPI, ) -> None: """Initialize a Sure Petcare Device.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type) + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) + self._attr_unique_id = ( + f"{self._surepy_entity.household_id}-{self._id}-connectivity" + ) @property def name(self) -> str: """Return the name of the device if any.""" return f"{self._name}_connectivity" - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._surepy_entity.household_id}-{self._id}-connectivity" - @property def available(self) -> bool: """Return true if entity is available.""" diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 231ede6474f..1f0804e0581 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -2,7 +2,7 @@ "domain": "surepetcare", "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", - "codeowners": ["@benleb"], + "codeowners": ["@benleb", "@danielhiversen"], "requirements": ["surepy==0.6.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index cfdb25fd412..681ef5b067c 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -46,8 +46,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class SurePetcareSensor(SensorEntity): - """A binary sensor implementation for Sure Petcare Entities.""" +class SureBattery(SensorEntity): + """A sensor implementation for Sure Petcare Entities.""" + + _attr_should_poll = False def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare sensor.""" @@ -57,9 +59,12 @@ class SurePetcareSensor(SensorEntity): self._surepy_entity: SurepyEntity = self._spc.states[_id] self._state: dict[str, Any] = {} - self._name = ( - f"{self._surepy_entity.type.name.capitalize()} " - f"{self._surepy_entity.name.capitalize()}" + + self._attr_device_class = DEVICE_CLASS_BATTERY + self._attr_name = f"{self._surepy_entity.type.name.capitalize()} {self._surepy_entity.name.capitalize()} Battery Level" + self._attr_unit_of_measurement = PERCENTAGE + self._attr_unique_id = ( + f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery" ) @property @@ -67,17 +72,12 @@ class SurePetcareSensor(SensorEntity): """Return true if entity is available.""" return bool(self._state) - @property - def should_poll(self) -> bool: - """Return true.""" - return False - @callback def _async_update(self) -> None: """Get the latest data and update the state.""" self._surepy_entity = self._spc.states[self._id] self._state = self._surepy_entity.raw_data()["status"] - _LOGGER.debug("%s -> self._state: %s", self._name, self._state) + _LOGGER.debug("%s -> self._state: %s", self.name, self._state) async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -86,37 +86,15 @@ class SurePetcareSensor(SensorEntity): ) self._async_update() - -class SureBattery(SurePetcareSensor): - """Sure Petcare Flap.""" - - @property - def name(self) -> str: - """Return the name of the device if any.""" - return f"{self._name} Battery Level" - @property def state(self) -> int | None: """Return battery level in percent.""" - battery_percent: int | None try: per_battery_voltage = self._state["battery"] / 4 voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - battery_percent = min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100) + return min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100) except (KeyError, TypeError): - battery_percent = None - - return battery_percent - - @property - def unique_id(self) -> str: - """Return an unique ID.""" - return f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery" - - @property - def device_class(self) -> str: - """Return the device class.""" - return DEVICE_CLASS_BATTERY + return None @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -130,8 +108,3 @@ class SureBattery(SurePetcareSensor): } return attributes - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return PERCENTAGE From 473ab98a670ec4945980ea23be46198f71eeebef Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 27 Jun 2021 00:09:39 +0000 Subject: [PATCH 546/750] [ci skip] Translation update --- .../components/dsmr/translations/de.json | 38 +++++++++++++- .../simplisafe/translations/pl.json | 2 +- .../components/zwave_js/translations/de.json | 50 +++++++++++++++++++ .../components/zwave_js/translations/ja.json | 26 ++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zwave_js/translations/ja.json diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json index 97d6739b787..fe94109634a 100644 --- a/homeassistant/components/dsmr/translations/de.json +++ b/homeassistant/components/dsmr/translations/de.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_communicate": "Kommunikation fehlgeschlagen", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_communicate": "Kommunikation fehlgeschlagen", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "DSMR-Version ausw\u00e4hlen", + "host": "Host", + "port": "Port" + }, + "title": "Verbindungsadresse ausw\u00e4hlen" + }, + "setup_serial": { + "data": { + "dsmr_version": "DSMR-Version ausw\u00e4hlen", + "port": "Ger\u00e4t w\u00e4hlen" + }, + "title": "Ger\u00e4t" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB-Ger\u00e4te-Pfad" + }, + "title": "Pfad" + }, + "user": { + "data": { + "type": "Verbindungstyp" + }, + "title": "Verbindungstyp ausw\u00e4hlen" + } } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 3d99e1f5145..260d9d6b148 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -19,7 +19,7 @@ "data": { "password": "Has\u0142o" }, - "description": "Tw\u00f3j dost\u0119pu wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.", + "description": "Tw\u00f3j dost\u0119p wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index c4672112fe5..a5c637b51fa 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -51,5 +51,55 @@ } } }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Die Discovery-Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.", + "addon_info_failed": "Die Informationen des Z-Wave JS-Add-On konnten nicht abgerufen werden.", + "addon_install_failed": "Die Installation des Z-Wave-JS-Add-ons ist fehlgeschlagen.", + "addon_set_config_failed": "Fehler beim Festlegen der Z-Wave JS-Konfiguration.", + "addon_start_failed": "Der Start des Z-Wave JS-Add-ons ist fehlgeschlagen.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "different_device": "Das angeschlossene USB-Ger\u00e4t ist nicht dasselbe, das zuvor f\u00fcr diesen Config-Eintrag konfiguriert wurde. Bitte erstelle stattdessen einen neuen Konfigurationseintrag f\u00fcr das neue Ger\u00e4t." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_ws_url": "Ung\u00fcltige Websocket-URL", + "unknown": "Unerwarteter Fehler" + }, + "progress": { + "install_addon": "Bitte warte, w\u00e4hrend die Installation des Z-Wave JS-Add-ons abgeschlossen wird. Dies kann einige Minuten dauern.", + "start_addon": "Bitte warte, w\u00e4hrend der Start des Z-Wave JS-Add-ons abgeschlossen wird. Dies kann einige Sekunden dauern." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Hardware emulieren", + "log_level": "Protokollstufe", + "network_key": "Netzwerkschl\u00fcssel", + "usb_path": "USB-Ger\u00e4te-Pfad" + }, + "title": "Gib die Konfiguration des Z-Wave JS-Add-ons ein" + }, + "install_addon": { + "title": "Die Installation des Z-Wave-JS-Add-ons hat begonnen" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Verwende das Supervisor Add-on Z-Wave JS" + }, + "description": "M\u00f6chtest du das Supervisor Add-on Z-Wave JS verwenden?", + "title": "Verbindungsmethode w\u00e4hlen" + }, + "start_addon": { + "title": "Das Z-Wave JS Add-on wird gestartet." + } + } + }, "title": "" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json new file mode 100644 index 00000000000..f7f882aa078 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -0,0 +1,26 @@ +{ + "options": { + "step": { + "configure_addon": { + "data": { + "log_level": "\u30ed\u30b0\u30ec\u30d9\u30eb", + "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af" + }, + "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" + }, + "install_addon": { + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" + }, + "on_supervisor": { + "data": { + "use_addon": "\u30a2\u30c9\u30aa\u30f3\u300cZ-Wave JS Supervisor\u300d\u306e\u4f7f\u7528" + }, + "description": "Z-Wave JS Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f", + "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" + }, + "start_addon": { + "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file From 6b08aebe5f91619fdead570a807ae3527524b950 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 27 Jun 2021 14:05:04 +0200 Subject: [PATCH 547/750] Add Forecast Solar integration (#52158) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + CODEOWNERS | 1 + .../components/forecast_solar/__init__.py | 79 ++++++ .../components/forecast_solar/config_flow.py | 119 +++++++++ .../components/forecast_solar/const.py | 89 +++++++ .../components/forecast_solar/manifest.json | 10 + .../components/forecast_solar/models.py | 17 ++ .../components/forecast_solar/sensor.py | 68 ++++++ .../components/forecast_solar/strings.json | 31 +++ .../forecast_solar/translations/en.json | 31 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/forecast_solar/__init__.py | 1 + tests/components/forecast_solar/conftest.py | 92 +++++++ .../forecast_solar/test_config_flow.py | 89 +++++++ tests/components/forecast_solar/test_init.py | 46 ++++ .../components/forecast_solar/test_sensor.py | 228 ++++++++++++++++++ 19 files changed, 920 insertions(+) create mode 100644 homeassistant/components/forecast_solar/__init__.py create mode 100644 homeassistant/components/forecast_solar/config_flow.py create mode 100644 homeassistant/components/forecast_solar/const.py create mode 100644 homeassistant/components/forecast_solar/manifest.json create mode 100644 homeassistant/components/forecast_solar/models.py create mode 100644 homeassistant/components/forecast_solar/sensor.py create mode 100644 homeassistant/components/forecast_solar/strings.json create mode 100644 homeassistant/components/forecast_solar/translations/en.json create mode 100644 tests/components/forecast_solar/__init__.py create mode 100644 tests/components/forecast_solar/conftest.py create mode 100644 tests/components/forecast_solar/test_config_flow.py create mode 100644 tests/components/forecast_solar/test_init.py create mode 100644 tests/components/forecast_solar/test_sensor.py diff --git a/.strict-typing b/.strict-typing index cde4d314cca..09578153163 100644 --- a/.strict-typing +++ b/.strict-typing @@ -30,6 +30,7 @@ homeassistant.components.dsmr.* homeassistant.components.dunehd.* homeassistant.components.elgato.* homeassistant.components.fitbit.* +homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* homeassistant.components.geo_location.* diff --git a/CODEOWNERS b/CODEOWNERS index 592aa5923ba..9a1f44cb1c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -163,6 +163,7 @@ homeassistant/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya +homeassistant/components/forecast_solar/* @klaasnicolaas @frenck homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py new file mode 100644 index 00000000000..b00e5f1c4ce --- /dev/null +++ b/homeassistant/components/forecast_solar/__init__.py @@ -0,0 +1,79 @@ +"""The Forecast.Solar integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from forecast_solar import ForecastSolar + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + CONF_AZIMUTH, + CONF_DAMPING, + CONF_DECLINATION, + CONF_MODULES_POWER, + DOMAIN, +) + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Forecast.Solar from a config entry.""" + api_key = entry.options.get(CONF_API_KEY) + # Our option flow may cause it to be an empty string, + # this if statement is here to catch that. + if not api_key: + api_key = None + + forecast = ForecastSolar( + api_key=api_key, + latitude=entry.data[CONF_LATITUDE], + longitude=entry.data[CONF_LONGITUDE], + declination=entry.options[CONF_DECLINATION], + azimuth=(entry.options[CONF_AZIMUTH] - 180), + kwp=(entry.options[CONF_MODULES_POWER] / 1000), + damping=entry.options.get(CONF_DAMPING, 0), + ) + + # Free account have a resolution of 1 hour, using that as the default + # update interval. Using a higher value for accounts with an API key. + update_interval = timedelta(hours=1) + if api_key is not None: + update_interval = timedelta(minutes=30) + + coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_method=forecast.estimate, + update_interval=update_interval, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(async_update_options)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py new file mode 100644 index 00000000000..256534da67a --- /dev/null +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for Forecast.Solar integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_AZIMUTH, + CONF_DAMPING, + CONF_DECLINATION, + CONF_MODULES_POWER, + DOMAIN, +) + + +class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Forecast.Solar.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> ForecastSolarOptionFlowHandler: + """Get the options flow for this handler.""" + return ForecastSolarOptionFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + if user_input is not None: + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_LATITUDE: user_input[CONF_LATITUDE], + CONF_LONGITUDE: user_input[CONF_LONGITUDE], + }, + options={ + CONF_AZIMUTH: user_input[CONF_AZIMUTH], + CONF_DECLINATION: user_input[CONF_DECLINATION], + CONF_MODULES_POWER: user_input[CONF_MODULES_POWER], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=self.hass.config.location_name + ): str, + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Required(CONF_DECLINATION, default=25): vol.All( + vol.Coerce(int), vol.Range(min=0, max=90) + ), + vol.Required(CONF_AZIMUTH, default=180): vol.All( + vol.Coerce(int), vol.Range(min=0, max=360) + ), + vol.Required(CONF_MODULES_POWER): vol.Coerce(int), + } + ), + ) + + +class ForecastSolarOptionFlowHandler(OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_API_KEY, + default=self.config_entry.options.get(CONF_API_KEY, ""), + ): str, + vol.Required( + CONF_DECLINATION, + default=self.config_entry.options[CONF_DECLINATION], + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=90)), + vol.Required( + CONF_AZIMUTH, + default=self.config_entry.options.get(CONF_AZIMUTH), + ): vol.All(vol.Coerce(int), vol.Range(min=-0, max=360)), + vol.Required( + CONF_MODULES_POWER, + default=self.config_entry.options[CONF_MODULES_POWER], + ): vol.Coerce(int), + vol.Optional( + CONF_DAMPING, + default=self.config_entry.options.get(CONF_DAMPING, 0.0), + ): vol.Coerce(float), + } + ), + ) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py new file mode 100644 index 00000000000..12aa1ee5362 --- /dev/null +++ b/homeassistant/components/forecast_solar/const.py @@ -0,0 +1,89 @@ +"""Constants for the Forecast.Solar integration.""" +from __future__ import annotations + +from typing import Final + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TIMESTAMP, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) + +from .models import ForecastSolarSensor + +DOMAIN = "forecast_solar" + +CONF_DECLINATION = "declination" +CONF_AZIMUTH = "azimuth" +CONF_MODULES_POWER = "modules power" +CONF_DAMPING = "damping" +ATTR_ENTRY_TYPE: Final = "entry_type" +ENTRY_TYPE_SERVICE: Final = "service" + +SENSORS: list[ForecastSolarSensor] = [ + ForecastSolarSensor( + key="energy_production_today", + name="Estimated Energy Production - Today", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + ForecastSolarSensor( + key="energy_production_tomorrow", + name="Estimated Energy Production - Tomorrow", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + ForecastSolarSensor( + key="power_highest_peak_time_today", + name="Highest Power Peak Time - Today", + device_class=DEVICE_CLASS_TIMESTAMP, + ), + ForecastSolarSensor( + key="power_highest_peak_time_tomorrow", + name="Highest Power Peak Time - Tomorrow", + device_class=DEVICE_CLASS_TIMESTAMP, + ), + ForecastSolarSensor( + key="power_production_now", + name="Estimated Power Production - Now", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=POWER_WATT, + ), + ForecastSolarSensor( + key="power_production_next_hour", + name="Estimated Power Production - Next Hour", + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + unit_of_measurement=POWER_WATT, + ), + ForecastSolarSensor( + key="power_production_next_12hours", + name="Estimated Power Production - Next 12 Hours", + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + unit_of_measurement=POWER_WATT, + ), + ForecastSolarSensor( + key="power_production_next_24hours", + name="Estimated Power Production - Next 24 Hours", + device_class=DEVICE_CLASS_POWER, + entity_registry_enabled_default=False, + unit_of_measurement=POWER_WATT, + ), + ForecastSolarSensor( + key="energy_current_hour", + name="Estimated Energy Production - This Hour", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + ForecastSolarSensor( + key="energy_next_hour", + name="Estimated Energy Production - Next Hour", + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), +] diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json new file mode 100644 index 00000000000..c17e8bd51f8 --- /dev/null +++ b/homeassistant/components/forecast_solar/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "forecast_solar", + "name": "Forecast.Solar", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/forecast_solar", + "requirements": ["forecast_solar==1.3.1"], + "codeowners": ["@klaasnicolaas", "@frenck"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/forecast_solar/models.py b/homeassistant/components/forecast_solar/models.py new file mode 100644 index 00000000000..d01f17fc975 --- /dev/null +++ b/homeassistant/components/forecast_solar/models.py @@ -0,0 +1,17 @@ +"""Models for the Forecast.Solar integration.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class ForecastSolarSensor: + """Represents an Forecast.Solar Sensor.""" + + key: str + name: str + + device_class: str | None = None + entity_registry_enabled_default: bool = True + state_class: str | None = None + unit_of_measurement: str | None = None diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py new file mode 100644 index 00000000000..a6b1927926e --- /dev/null +++ b/homeassistant/components/forecast_solar/sensor.py @@ -0,0 +1,68 @@ +"""Support for the Forecast.Solar sensor service.""" +from __future__ import annotations + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS +from .models import ForecastSolarSensor + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Defer sensor setup to the shared sensor module.""" + coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + ForecastSolarSensorEntity( + entry_id=entry.entry_id, coordinator=coordinator, sensor=sensor + ) + for sensor in SENSORS + ) + + +class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): + """Defines a Forcast.Solar sensor.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: DataUpdateCoordinator, + sensor: ForecastSolarSensor, + ) -> None: + """Initialize Forcast.Solar sensor.""" + super().__init__(coordinator=coordinator) + self._sensor = sensor + + self.entity_id = f"{SENSOR_DOMAIN}.{sensor.key}" + self._attr_device_class = sensor.device_class + self._attr_entity_registry_enabled_default = ( + sensor.entity_registry_enabled_default + ) + self._attr_name = sensor.name + self._attr_state_class = sensor.state_class + self._attr_unique_id = f"{entry_id}_{sensor.key}" + self._attr_unit_of_measurement = sensor.unit_of_measurement + + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, + ATTR_NAME: "Solar Production Forecast", + ATTR_MANUFACTURER: "Forecast.Solar", + ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, + } + + @property + def state(self) -> StateType: + """Return the state of the sensor.""" + state: StateType = getattr(self.coordinator.data, self._sensor.key) + return state diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json new file mode 100644 index 00000000000..eb98fc79297 --- /dev/null +++ b/homeassistant/components/forecast_solar/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "description": "Fill in the data of your solar panels. Please refer to the documentation if a field is unclear.", + "data": { + "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", + "declination": "Declination (0 = Horizontal, 90 = Vertical)", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "modules power": "Total Watt peak power of your solar modules", + "name": "[%key:common::config_flow::data::name%]" + } + } + } + }, + "options": { + "step": { + "init": { + "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear.", + "data": { + "api_key": "Forecast.Solar API Key (optional)", + "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", + "damping": "Damping factor: adjusts the results in the morning and evening", + "declination": "Declination (0 = Horizontal, 90 = Vertical)", + "modules power": "Total Watt peak power of your solar modules" + } + } + } + } +} diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json new file mode 100644 index 00000000000..6de9cddc567 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", + "declination": "Declination (0 = Horizontal, 90 = Vertical)", + "latitude": "Latitude", + "longitude": "Longitude", + "modules power": "Total Watt peak power of your solar modules", + "name": "Name" + }, + "description": "Fill in the data of your solar panels. Please refer to the documentation if a field is unclear." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API Key (optional)", + "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", + "damping": "Damping factor: adjusts the results in the morning and evening", + "declination": "Declination (0 = Horizontal, 90 = Vertical)", + "modules power": "Total Watt peak power of your solar modules" + }, + "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7af0bfd129e..69886e370f7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -77,6 +77,7 @@ FLOWS = [ "flo", "flume", "flunearyou", + "forecast_solar", "forked_daapd", "foscam", "freebox", diff --git a/mypy.ini b/mypy.ini index 0c4fe1bf7a8..4472311279f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -341,6 +341,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.forecast_solar.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fritzbox.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 06b13349058..8feeebdb925 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -623,6 +623,9 @@ fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 +# homeassistant.components.forecast_solar +forecast_solar==1.3.1 + # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de7cd4ed7f5..45cb827bc6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,6 +335,9 @@ fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 +# homeassistant.components.forecast_solar +forecast_solar==1.3.1 + # homeassistant.components.freebox freebox-api==0.0.10 diff --git a/tests/components/forecast_solar/__init__.py b/tests/components/forecast_solar/__init__.py new file mode 100644 index 00000000000..e3c1f710aef --- /dev/null +++ b/tests/components/forecast_solar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Forecast Solar integration.""" diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py new file mode 100644 index 00000000000..c2b5fc08181 --- /dev/null +++ b/tests/components/forecast_solar/conftest.py @@ -0,0 +1,92 @@ +"""Fixtures for Forecast.Solar integration tests.""" + +import datetime +from typing import Generator +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.forecast_solar.const import ( + CONF_AZIMUTH, + CONF_DAMPING, + CONF_DECLINATION, + CONF_MODULES_POWER, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +async def mock_persistent_notification(hass: HomeAssistant) -> None: + """Set up component for persistent notifications.""" + await async_setup_component(hass, "persistent_notification", {}) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Green House", + unique_id="unique", + domain=DOMAIN, + data={ + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + }, + options={ + CONF_API_KEY: "abcdef12345", + CONF_DECLINATION: 30, + CONF_AZIMUTH: 190, + CONF_MODULES_POWER: 5100, + CONF_DAMPING: 0.5, + }, + ) + + +@pytest.fixture +def mock_forecast_solar() -> Generator[None, MagicMock, None]: + """Return a mocked Forecast.Solar client.""" + with patch( + "homeassistant.components.forecast_solar.ForecastSolar", autospec=True + ) as forecast_solar_mock: + forecast_solar = forecast_solar_mock.return_value + + estimate = MagicMock() + estimate.timezone = "Europe/Amsterdam" + estimate.energy_production_today = 100 + estimate.energy_production_tomorrow = 200 + estimate.power_production_now = 300 + estimate.power_highest_peak_time_today = datetime.datetime( + 2021, 6, 27, 13, 0, tzinfo=datetime.timezone.utc + ) + estimate.power_highest_peak_time_tomorrow = datetime.datetime( + 2021, 6, 27, 14, 0, tzinfo=datetime.timezone.utc + ) + estimate.power_production_next_hour = 400 + estimate.power_production_next_6hours = 500 + estimate.power_production_next_12hours = 600 + estimate.power_production_next_24hours = 700 + estimate.energy_current_hour = 800 + estimate.energy_next_hour = 900 + + forecast_solar.estimate.return_value = estimate + yield forecast_solar + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_forecast_solar: MagicMock, +) -> MockConfigEntry: + """Set up the Forecast.Solar integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py new file mode 100644 index 00000000000..ac950d38b51 --- /dev/null +++ b/tests/components/forecast_solar/test_config_flow.py @@ -0,0 +1,89 @@ +"""Test the Forecast.Solar config flow.""" +from unittest.mock import patch + +from homeassistant.components.forecast_solar.const import ( + CONF_AZIMUTH, + CONF_DAMPING, + CONF_DECLINATION, + CONF_MODULES_POWER, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +async def test_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.forecast_solar.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + CONF_AZIMUTH: 142, + CONF_DECLINATION: 42, + CONF_MODULES_POWER: 4242, + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Name" + assert result2.get("data") == { + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + } + assert result2.get("options") == { + CONF_AZIMUTH: 142, + CONF_DECLINATION: 42, + CONF_MODULES_POWER: 4242, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test config flow options.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "solarPOWER!", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("data") == { + CONF_API_KEY: "solarPOWER!", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + } diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py new file mode 100644 index 00000000000..719041aaf58 --- /dev/null +++ b/tests/components/forecast_solar/test_init.py @@ -0,0 +1,46 @@ +"""Tests for the Forecast.Solar integration.""" +from unittest.mock import MagicMock, patch + +from forecast_solar import ForecastSolarConnectionError + +from homeassistant.components.forecast_solar.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_forecast_solar: MagicMock, +) -> None: + """Test the Forecast.Solar configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + + +@patch( + "homeassistant.components.forecast_solar.ForecastSolar.estimate", + side_effect=ForecastSolarConnectionError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Forecast.Solar configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py new file mode 100644 index 00000000000..a3513b86a5d --- /dev/null +++ b/tests/components/forecast_solar/test_sensor.py @@ -0,0 +1,228 @@ +"""Tests for the sensors provided by the Forecast.Solar integration.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.forecast_solar.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TIMESTAMP, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Forecast.Solar sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.energy_production_today") + entry = entity_registry.async_get("sensor.energy_production_today") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_energy_production_today" + assert state.state == "100" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Estimated Energy Production - Today" + ) + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.energy_production_tomorrow") + entry = entity_registry.async_get("sensor.energy_production_tomorrow") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_energy_production_tomorrow" + assert state.state == "200" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Estimated Energy Production - Tomorrow" + ) + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.power_highest_peak_time_today") + entry = entity_registry.async_get("sensor.power_highest_peak_time_today") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" + assert state.state == "2021-06-27 13:00:00+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.power_highest_peak_time_tomorrow") + entry = entity_registry.async_get("sensor.power_highest_peak_time_tomorrow") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" + assert state.state == "2021-06-27 14:00:00+00:00" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" + ) + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.power_production_now") + entry = entity_registry.async_get("sensor.power_production_now") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_power_production_now" + assert state.state == "300" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.energy_current_hour") + entry = entity_registry.async_get("sensor.energy_current_hour") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_energy_current_hour" + assert state.state == "800" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Estimated Energy Production - This Hour" + ) + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.energy_next_hour") + entry = entity_registry.async_get("sensor.energy_next_hour") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_energy_next_hour" + assert state.state == "900" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Estimated Energy Production - Next Hour" + ) + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")} + assert device_entry.manufacturer == "Forecast.Solar" + assert device_entry.name == "Solar Production Forecast" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.power_production_next_12hours", + "sensor.power_production_next_24hours", + "sensor.power_production_next_hour", + ), +) +async def test_disabled_by_default( + hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str +) -> None: + """Test the Forecast.Solar sensors that are disabled by default.""" + entity_registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state is None + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == er.DISABLED_INTEGRATION + + +@pytest.mark.parametrize( + "key,name,value", + [ + ( + "power_production_next_12hours", + "Estimated Power Production - Next 12 Hours", + "600", + ), + ( + "power_production_next_24hours", + "Estimated Power Production - Next 24 Hours", + "700", + ), + ( + "power_production_next_hour", + "Estimated Power Production - Next Hour", + "400", + ), + ], +) +async def test_enabling_disable_by_default( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_forecast_solar: MagicMock, + key: str, + name: str, + value: str, +) -> None: + """Test the Forecast.Solar sensors that are disabled by default.""" + entry_id = mock_config_entry.entry_id + entity_id = f"{SENSOR_DOMAIN}.{key}" + entity_registry = er.async_get(hass) + + # Pre-create registry entry for disabled by default sensor + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{entry_id}_{key}", + suggested_object_id=key, + disabled_by=None, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + entry = entity_registry.async_get(entity_id) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_{key}" + assert state.state == value + assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert ATTR_ICON not in state.attributes From 0d6e73236a43c4f347ad39ed6c269c34a77a9544 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 27 Jun 2021 14:53:55 +0200 Subject: [PATCH 548/750] Upgrade pyrituals 0.0.3 -> 0.0.4 (#52209) --- homeassistant/components/rituals_perfume_genie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 756af10f33b..2736b960751 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,7 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": ["pyrituals==0.0.3"], + "requirements": ["pyrituals==0.0.4"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 8feeebdb925..bd8380d455a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.3 +pyrituals==0.0.4 # homeassistant.components.ruckus_unleashed pyruckus==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45cb827bc6b..9285abfe32c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.3 +pyrituals==0.0.4 # homeassistant.components.ruckus_unleashed pyruckus==0.12 From f1b5183e475168cb429abfd9bf98f972b78d0d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 27 Jun 2021 16:49:22 +0200 Subject: [PATCH 549/750] Tibber power factor (#52223) --- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tibber/sensor.py | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 57b329765a9..a915db8a665 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.17.0"], + "requirements": ["pyTibber==0.18.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 330e9d5c61d..c48a201d796 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, @@ -18,6 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, VOLT, @@ -127,6 +129,12 @@ RT_SENSOR_MAP = { STATE_CLASS_MEASUREMENT, ], "accumulatedCost": ["accumulated cost", None, None, STATE_CLASS_MEASUREMENT], + "powerFactor": [ + "power factor", + DEVICE_CLASS_POWER_FACTOR, + PERCENTAGE, + STATE_CLASS_MEASUREMENT, + ], } @@ -395,6 +403,8 @@ class TibberRtDataHandler: for sensor_type, state in live_measurement.items(): if state is None or sensor_type not in RT_SENSOR_MAP: continue + if sensor_type == "powerFactor": + state *= 100.0 if sensor_type in self._entities: async_dispatcher_send( self.hass, diff --git a/requirements_all.txt b/requirements_all.txt index bd8380d455a..de0a007e86c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1273,7 +1273,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.17.0 +pyTibber==0.18.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9285abfe32c..c46a29eab26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -710,7 +710,7 @@ pyMetno==0.8.3 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.17.0 +pyTibber==0.18.0 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From 75d29b3d9ee9a9b04d8f5a89ffd6fe13efe606cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Jun 2021 16:57:30 +0200 Subject: [PATCH 550/750] Upgrade watchdog to 2.1.3 (#52224) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 9f89045b28e..709e95f476b 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.1.2"], + "requirements": ["watchdog==2.1.3"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index de0a007e86c..af5920a1d84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2350,7 +2350,7 @@ wallbox==0.4.4 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.2 +watchdog==2.1.3 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c46a29eab26..7093e842ce3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1277,7 +1277,7 @@ wakeonlan==2.0.1 wallbox==0.4.4 # homeassistant.components.folder_watcher -watchdog==2.1.2 +watchdog==2.1.3 # homeassistant.components.wiffi wiffi==1.0.1 From 3f49cdf9bf0775129219bb08823ac63d6d87154b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Jun 2021 16:58:08 +0200 Subject: [PATCH 551/750] DSMR: Use entry unload to unsub update listener (#52220) --- homeassistant/components/dsmr/__init__.py | 9 ++------- homeassistant/components/dsmr/const.py | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 3ebac7b42fc..0e238363fc0 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -5,7 +5,7 @@ from contextlib import suppress from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS +from .const import DATA_TASK, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -14,9 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {} hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - listener = entry.add_update_listener(async_update_options) - hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] = listener + entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -24,7 +22,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" task = hass.data[DOMAIN][entry.entry_id][DATA_TASK] - listener = hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] # Cancel the reconnect task task.cancel() @@ -33,8 +30,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - listener() - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 5c75dcc61ca..b26caa5c865 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -36,7 +36,6 @@ DEFAULT_PRECISION = 3 DEFAULT_RECONNECT_INTERVAL = 30 DEFAULT_TIME_BETWEEN_UPDATE = 30 -DATA_LISTENER = "listener" DATA_TASK = "task" DEVICE_NAME_ENERGY = "Energy Meter" From a824313e9f8498bc64430b548f62c2d23a1dbc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 27 Jun 2021 17:25:54 +0200 Subject: [PATCH 552/750] Clean up Surepetcare sensor (#52219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- .../components/surepetcare/sensor.py | 62 ++++++++----------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 681ef5b067c..fbc8222f292 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from surepy.entities import SurepyEntity from surepy.enums import EntityType @@ -57,27 +56,41 @@ class SureBattery(SensorEntity): self._id = _id self._spc: SurePetcareAPI = spc - self._surepy_entity: SurepyEntity = self._spc.states[_id] - self._state: dict[str, Any] = {} + surepy_entity: SurepyEntity = self._spc.states[_id] self._attr_device_class = DEVICE_CLASS_BATTERY - self._attr_name = f"{self._surepy_entity.type.name.capitalize()} {self._surepy_entity.name.capitalize()} Battery Level" + self._attr_name = f"{surepy_entity.type.name.capitalize()} {surepy_entity.name.capitalize()} Battery Level" self._attr_unit_of_measurement = PERCENTAGE self._attr_unique_id = ( - f"{self._surepy_entity.household_id}-{self._surepy_entity.id}-battery" + f"{surepy_entity.household_id}-{surepy_entity.id}-battery" ) - @property - def available(self) -> bool: - """Return true if entity is available.""" - return bool(self._state) - @callback def _async_update(self) -> None: """Get the latest data and update the state.""" - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.raw_data()["status"] - _LOGGER.debug("%s -> self._state: %s", self.name, self._state) + surepy_entity = self._spc.states[self._id] + state = surepy_entity.raw_data()["status"] + + self._attr_available = bool(state) + try: + per_battery_voltage = state["battery"] / 4 + voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW + self._attr_state = min( + int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100 + ) + except (KeyError, TypeError): + self._attr_state = None + + if state: + voltage_per_battery = float(state["battery"]) / 4 + self._attr_extra_state_attributes = { + ATTR_VOLTAGE: f"{float(state['battery']):.2f}", + f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", + } + else: + self._attr_extra_state_attributes = None + self.async_write_ha_state() + _LOGGER.debug("%s -> state: %s", self.name, state) async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -85,26 +98,3 @@ class SureBattery(SensorEntity): async_dispatcher_connect(self.hass, TOPIC_UPDATE, self._async_update) ) self._async_update() - - @property - def state(self) -> int | None: - """Return battery level in percent.""" - try: - per_battery_voltage = self._state["battery"] / 4 - voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - return min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100) - except (KeyError, TypeError): - return None - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return state attributes.""" - attributes = None - if self._state: - voltage_per_battery = float(self._state["battery"]) / 4 - attributes = { - ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", - f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", - } - - return attributes From a788b6ebc1a33234558bffde296bc850006e1138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 27 Jun 2021 17:27:33 +0200 Subject: [PATCH 553/750] Clean up surepetcare binary sensor (#52217) Co-authored-by: Martin Hjelmare --- .../components/surepetcare/__init__.py | 11 +- .../components/surepetcare/binary_sensor.py | 126 ++++++++---------- 2 files changed, 58 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 3283b4c97c9..8f0c2311518 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -90,12 +90,10 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async_track_time_interval(hass, spc.async_update, SCAN_INTERVAL) # load platforms - hass.async_create_task( - hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config) - ) - hass.async_create_task( - hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) - ) + for platform in PLATFORMS: + hass.async_create_task( + hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config) + ) async def handle_set_lock_state(call): """Call when setting the lock state.""" @@ -150,6 +148,7 @@ class SurePetcareAPI: self.states = await self.surepy.get_entities() except SurePetcareError as error: _LOGGER.error("Unable to fetch data: %s", error) + return async_dispatcher_send(self.hass, TOPIC_UPDATE) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 2e0dcf79872..ca7b7378127 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,11 +1,11 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" from __future__ import annotations +from abc import abstractmethod import logging -from typing import Any from surepy.entities import SurepyEntity -from surepy.enums import EntityType, Location, SureEnum +from surepy.enums import EntityType, Location from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -67,31 +67,28 @@ class SurePetcareBinarySensor(BinarySensorEntity): self._id = _id self._spc: SurePetcareAPI = spc - self._surepy_entity: SurepyEntity = self._spc.states[self._id] - self._state: SureEnum | dict[str, Any] = None + surepy_entity: SurepyEntity = self._spc.states[self._id] # cover special case where a device has no name set - if self._surepy_entity.name: - name = self._surepy_entity.name + if surepy_entity.name: + name = surepy_entity.name else: - name = f"Unnamed {self._surepy_entity.type.name.capitalize()}" + name = f"Unnamed {surepy_entity.type.name.capitalize()}" - self._name = f"{self._surepy_entity.type.name.capitalize()} {name.capitalize()}" + self._name = f"{surepy_entity.type.name.capitalize()} {name.capitalize()}" self._attr_device_class = device_class - self._attr_unique_id = f"{self._surepy_entity.household_id}-{self._id}" + self._attr_unique_id = f"{surepy_entity.household_id}-{self._id}" @property def name(self) -> str: """Return the name of the device if any.""" return self._name + @abstractmethod @callback def _async_update(self) -> None: """Get the latest data and update the state.""" - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.raw_data()["status"] - _LOGGER.debug("%s -> self._state: %s", self._name, self._state) async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -108,29 +105,23 @@ class Hub(SurePetcareBinarySensor): """Initialize a Sure Petcare Hub.""" super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) - @property - def available(self) -> bool: - """Return true if entity is available.""" - return bool(self._state["online"]) - - @property - def is_on(self) -> bool: - """Return true if entity is online.""" - return self.available - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the device.""" - attributes = None - if self._surepy_entity.raw_data(): - attributes = { - "led_mode": int(self._surepy_entity.raw_data()["status"]["led_mode"]), + @callback + def _async_update(self) -> None: + """Get the latest data and update the state.""" + surepy_entity = self._spc.states[self._id] + state = surepy_entity.raw_data()["status"] + self._attr_is_on = self._attr_available = bool(state["online"]) + if surepy_entity.raw_data(): + self._attr_extra_state_attributes = { + "led_mode": int(surepy_entity.raw_data()["status"]["led_mode"]), "pairing_mode": bool( - self._surepy_entity.raw_data()["status"]["pairing_mode"] + surepy_entity.raw_data()["status"]["pairing_mode"] ), } - - return attributes + else: + self._attr_extra_state_attributes = None + _LOGGER.debug("%s -> state: %s", self._name, state) + self.async_write_ha_state() class Pet(SurePetcareBinarySensor): @@ -140,29 +131,24 @@ class Pet(SurePetcareBinarySensor): """Initialize a Sure Petcare Pet.""" super().__init__(_id, spc, DEVICE_CLASS_PRESENCE) - @property - def is_on(self) -> bool: - """Return true if entity is at home.""" - try: - return bool(Location(self._state.where) == Location.INSIDE) - except (KeyError, TypeError): - return False - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the device.""" - attributes = None - if self._state: - attributes = {"since": self._state.since, "where": self._state.where} - - return attributes - @callback def _async_update(self) -> None: """Get the latest data and update the state.""" - self._surepy_entity = self._spc.states[self._id] - self._state = self._surepy_entity.location - _LOGGER.debug("%s -> self._state: %s", self._name, self._state) + surepy_entity = self._spc.states[self._id] + state = surepy_entity.location + try: + self._attr_is_on = bool(Location(state.where) == Location.INSIDE) + except (KeyError, TypeError): + self._attr_is_on = False + if state: + self._attr_extra_state_attributes = { + "since": state.since, + "where": state.where, + } + else: + self._attr_extra_state_attributes = None + _LOGGER.debug("%s -> state: %s", self._name, state) + self.async_write_ha_state() class DeviceConnectivity(SurePetcareBinarySensor): @@ -176,7 +162,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Initialize a Sure Petcare Device.""" super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY) self._attr_unique_id = ( - f"{self._surepy_entity.household_id}-{self._id}-connectivity" + f"{self._spc.states[self._id].household_id}-{self._id}-connectivity" ) @property @@ -184,24 +170,18 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Return the name of the device if any.""" return f"{self._name}_connectivity" - @property - def available(self) -> bool: - """Return true if entity is available.""" - return bool(self._state) - - @property - def is_on(self) -> bool: - """Return true if entity is online.""" - return self.available - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the device.""" - attributes = None - if self._state: - attributes = { - "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}', - "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}', + @callback + def _async_update(self) -> None: + """Get the latest data and update the state.""" + surepy_entity = self._spc.states[self._id] + state = surepy_entity.raw_data()["status"] + self._attr_is_on = self._attr_available = bool(self.state) + if state: + self._attr_extra_state_attributes = { + "device_rssi": f'{state["signal"]["device_rssi"]:.2f}', + "hub_rssi": f'{state["signal"]["hub_rssi"]:.2f}', } - - return attributes + else: + self._attr_extra_state_attributes = None + _LOGGER.debug("%s -> state: %s", self._name, state) + self.async_write_ha_state() From 89cdda9fe6a1a5af52088e15e36f89b80685d94b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 27 Jun 2021 18:31:07 +0200 Subject: [PATCH 554/750] Add idle hvac_action to KNX climate (#52006) * add idle hvac_action and command_value extra_state_attribute * use class attribute for unit --- homeassistant/components/knx/climate.py | 18 +++++++++++++++++- homeassistant/components/knx/schema.py | 4 ++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index b43f0efe7f0..aeef4a35c29 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -28,6 +28,7 @@ from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, DOMAIN, PRESET_MODES from .knx_entity import KnxEntity from .schema import ClimateSchema +ATTR_COMMAND_VALUE = "command_value" CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} @@ -156,10 +157,14 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate: temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP], group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS), group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS), + on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], + group_address_active_state=config.get(ClimateSchema.CONF_ACTIVE_STATE_ADDRESS), + group_address_command_value_state=config.get( + ClimateSchema.CONF_COMMAND_VALUE_STATE_ADDRESS + ), min_temp=config.get(ClimateSchema.CONF_MIN_TEMP), max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), mode=climate_mode, - on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], ) @@ -256,6 +261,8 @@ class KNXClimate(KnxEntity, ClimateEntity): """ if self._device.supports_on_off and not self._device.is_on: return CURRENT_HVAC_OFF + if self._device.is_active is False: + return CURRENT_HVAC_IDLE if self._device.mode is not None and self._device.mode.supports_controller_mode: return CURRENT_HVAC_ACTIONS.get( self._device.mode.controller_mode.value, CURRENT_HVAC_IDLE @@ -311,6 +318,15 @@ class KNXClimate(KnxEntity, ClimateEntity): await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return device specific state attributes.""" + attr: dict[str, Any] = {} + + if self._device.command_value.initialized: + attr[ATTR_COMMAND_VALUE] = self._device.command_value.value + return attr + async def async_added_to_hass(self) -> None: """Store register state change callback.""" await super().async_added_to_hass() diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index f863efec685..4604eaf1096 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -259,6 +259,7 @@ class ClimateSchema(KNXPlatformSchema): PLATFORM_NAME = SupportedPlatforms.CLIMATE.value + CONF_ACTIVE_STATE_ADDRESS = "active_state_address" CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address" CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode" @@ -274,6 +275,7 @@ class ClimateSchema(KNXPlatformSchema): CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address" CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address" CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address" + CONF_COMMAND_VALUE_STATE_ADDRESS = "command_value_state_address" CONF_HEAT_COOL_ADDRESS = "heat_cool_address" CONF_HEAT_COOL_STATE_ADDRESS = "heat_cool_state_address" CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = ( @@ -332,6 +334,8 @@ class ClimateSchema(KNXPlatformSchema): vol.Optional(CONF_SETPOINT_SHIFT_MODE): vol.Maybe( vol.All(vol.Upper, cv.enum(SetpointShiftMode)) ), + vol.Optional(CONF_ACTIVE_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_COMMAND_VALUE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator, vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator, From 253310aaa44a2f80c1fe0886a2c27f9f417fbe6d Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 27 Jun 2021 18:34:41 +0200 Subject: [PATCH 555/750] Add respond_to_read option to KNX switch (#51790) --- homeassistant/components/knx/schema.py | 1 + homeassistant/components/knx/switch.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 4604eaf1096..079dc7363bf 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -716,6 +716,7 @@ class SwitchSchema(KNXPlatformSchema): { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_INVERT, default=False): cv.boolean, + vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index bb7e582ce15..cca0ffde853 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN, KNX_ADDRESS +from .const import CONF_RESPOND_TO_READ, DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import SwitchSchema @@ -48,6 +48,7 @@ class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): name=config[CONF_NAME], group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + respond_to_read=config[CONF_RESPOND_TO_READ], invert=config[SwitchSchema.CONF_INVERT], ) ) From a9bd7da79d1c5213d2fcf996114963e5e6286791 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sun, 27 Jun 2021 18:38:07 +0200 Subject: [PATCH 556/750] Remove Rituals room size number entity (#52200) --- .../rituals_perfume_genie/number.py | 46 +------------------ 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index d7f8a3677b7..5fde2adcb6f 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM, SPEED +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, SPEED from .entity import DiffuserEntity _LOGGER = logging.getLogger(__name__) @@ -22,7 +22,6 @@ MIN_ROOM_SIZE = 1 MAX_ROOM_SIZE = 4 PERFUME_AMOUNT_SUFFIX = " Perfume Amount" -ROOM_SIZE_SUFFIX = " Room Size" async def async_setup_entry( @@ -37,7 +36,6 @@ async def async_setup_entry( for hublot, diffuser in diffusers.items(): coordinator = coordinators[hublot] entities.append(DiffuserPerfumeAmount(diffuser, coordinator)) - entities.append(DiffuserRoomSize(diffuser, coordinator)) async_add_entities(entities) @@ -82,45 +80,3 @@ class DiffuserPerfumeAmount(NumberEntity, DiffuserEntity): MIN_PERFUME_AMOUNT, MAX_PERFUME_AMOUNT, ) - - -class DiffuserRoomSize(NumberEntity, DiffuserEntity): - """Representation of a diffuser room size number.""" - - def __init__( - self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator - ) -> None: - """Initialize the diffuser room size number.""" - super().__init__(diffuser, coordinator, ROOM_SIZE_SUFFIX) - - @property - def icon(self) -> str: - """Return the icon of the room size entity.""" - return "mdi:ruler-square" - - @property - def value(self) -> int: - """Return the current room size.""" - return self._diffuser.hub_data[ATTRIBUTES][ROOM] - - @property - def min_value(self) -> int: - """Return the minimum room size.""" - return MIN_ROOM_SIZE - - @property - def max_value(self) -> int: - """Return the maximum room size.""" - return MAX_ROOM_SIZE - - async def async_set_value(self, value: float) -> None: - """Set the room size.""" - if value.is_integer() and MIN_ROOM_SIZE <= value <= MAX_ROOM_SIZE: - await self._diffuser.set_room_size(int(value)) - else: - _LOGGER.warning( - "Can't set the room size to %s. Room size must be an integer between %s and %s, inclusive", - value, - MIN_ROOM_SIZE, - MAX_ROOM_SIZE, - ) From fad7e43c4f34a3c29152698688c1549d977082c5 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 27 Jun 2021 09:39:40 -0700 Subject: [PATCH 557/750] Add state attribute to SmartTub reminders for days remaining (#51825) --- homeassistant/components/smarttub/binary_sensor.py | 7 +++++-- tests/components/smarttub/test_binary_sensor.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 331c0b7e3d7..7bb4e8cc9e0 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -29,9 +29,11 @@ ATTR_CREATED_AT = "created_at" ATTR_UPDATED_AT = "updated_at" # how many days to snooze the reminder for -ATTR_SNOOZE_DAYS = "days" +ATTR_REMINDER_DAYS = "days" SNOOZE_REMINDER_SCHEMA = { - vol.Required(ATTR_SNOOZE_DAYS): vol.All(vol.Coerce(int), vol.Range(min=10, max=120)) + vol.Required(ATTR_REMINDER_DAYS): vol.All( + vol.Coerce(int), vol.Range(min=10, max=120) + ) } @@ -117,6 +119,7 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): """Return the state attributes.""" return { ATTR_REMINDER_SNOOZED: self.reminder.snoozed, + ATTR_REMINDER_DAYS: self.reminder.remaining_days, } async def async_snooze(self, days): diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index 16b4f60d3e4..98d404ef600 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -30,6 +30,7 @@ async def test_reminders(spa, setup_entry, hass): assert state is not None assert state.state == STATE_OFF assert state.attributes["snoozed"] is False + assert state.attributes["days"] == 2 @pytest.fixture From dafddce446943993916076fd6805da2b3da8d7ca Mon Sep 17 00:00:00 2001 From: ryansun96 Date: Sun, 27 Jun 2021 12:59:27 -0400 Subject: [PATCH 558/750] Update base image to 2021.06.2 (#52190) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index d2370b14773..c3a3eec0bee 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.1", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.1", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.1", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.1", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.1" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.2", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.2", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.2", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.2", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.2" }, "labels": { "io.hass.type": "core", From eedf1c3ebe9d391bc1654c88d86ee51669b08825 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Jun 2021 19:02:51 +0200 Subject: [PATCH 559/750] Reject requests from the proxy itself (#52073) * Reject requests from the proxy itself * Adjust tests --- homeassistant/components/http/forwarded.py | 8 ++++-- tests/components/http/test_forwarded.py | 33 ++++++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 18bc51af1d1..684dbbb9e2b 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -129,9 +129,11 @@ def async_setup_forwarded( overrides["remote"] = str(forwarded_ip) break else: - # If all the IP addresses are from trusted networks, take the left-most. - forwarded_for_index = -1 - overrides["remote"] = str(forwarded_for[-1]) + _LOGGER.warning( + "Request originated directly from a trusted proxy included in X-Forwarded-For: %s, this is likely a miss configuration and will be rejected", + forwarded_for_headers, + ) + raise HTTPBadRequest() # Handle X-Forwarded-Proto forwarded_proto_headers: list[str] = request.headers.getall( diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 400a1f32729..8d8467b699f 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -43,15 +43,9 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): @pytest.mark.parametrize( "trusted_proxies,x_forwarded_for,remote", [ - ( - ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], - "10.10.10.10, 1.1.1.1", - "10.10.10.10", - ), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123,2.2.2.2,1.1.1.1", "2.2.2.2"), (["127.0.0.0/24"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "1.1.1.1"), - (["127.0.0.0/24"], "127.0.0.1", "127.0.0.1"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 1.1.1.1", "123.123.123.123"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.1"], "255.255.255.255", "255.255.255.255"), @@ -83,6 +77,33 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 +@pytest.mark.parametrize( + "trusted_proxies,x_forwarded_for", + [ + ( + ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], + "10.10.10.10, 1.1.1.1", + ), + (["127.0.0.0/24"], "127.0.0.1"), + ], +) +async def test_x_forwarded_for_from_trusted_proxy_rejected( + trusted_proxies, x_forwarded_for, aiohttp_client +): + """Test that we reject forwarded requests from proxy server itself.""" + + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded( + app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + ) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) + + assert resp.status == 400 + + async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): """Test that we warn when processing is disabled, but proxy has been detected.""" From aececdfeb9a8705e8b990c875b928db5939182cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sun, 27 Jun 2021 19:43:31 +0200 Subject: [PATCH 560/750] Update pyfronius to 0.5.2 (#52216) * Update the pyfronius package to version 0.5.2 this automatically introduces support for API V0 of fronius devices * Update requirements --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 4f48bc1aecc..d526fc90b32 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -2,7 +2,7 @@ "domain": "fronius", "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.4.6"], + "requirements": ["pyfronius==0.5.2"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index af5920a1d84..9921e527e04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pyforked-daapd==0.1.11 pyfritzhome==0.4.2 # homeassistant.components.fronius -pyfronius==0.4.6 +pyfronius==0.5.2 # homeassistant.components.ifttt pyfttt==0.3 From 543e1a0f9c1888dd5d20165592f13035e2e6295f Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Sun, 27 Jun 2021 11:30:49 -0700 Subject: [PATCH 561/750] Make PjLink power toggle more robust (#51821) Occasionally, this integration misses events (or maybe they never get sent) from the projector and gets "stuck" in the wrong power state. Currently, this prevents this integration from changing the power state as it thinks its already in the correct state. Only way to resolve this is to reboot home assistant. This PR makes it a little more resilient by attempting to send the correct command even when it thinks it's already in the correct state. --- homeassistant/components/pjlink/media_player.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 74536ba8393..3ecd3d26ae1 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -157,15 +157,13 @@ class PjLinkDevice(MediaPlayerEntity): def turn_off(self): """Turn projector off.""" - if self._pwstate == STATE_ON: - with self.projector() as projector: - projector.set_power("off") + with self.projector() as projector: + projector.set_power("off") def turn_on(self): """Turn projector on.""" - if self._pwstate == STATE_OFF: - with self.projector() as projector: - projector.set_power("on") + with self.projector() as projector: + projector.set_power("on") def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" From 120fd633b242a273b938bbc878d153f4a8f99b9e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 27 Jun 2021 20:33:20 +0200 Subject: [PATCH 562/750] Add mysensors sensor platform test foundation (#51548) --- tests/components/mysensors/conftest.py | 150 +++++++++++++++++- tests/components/mysensors/test_sensor.py | 10 ++ .../fixtures/mysensors/gps_sensor_state.json | 21 +++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 tests/components/mysensors/test_sensor.py create mode 100644 tests/fixtures/mysensors/gps_sensor_state.json diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 7a4733e8ce2..8fbe9486352 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -1,10 +1,158 @@ """Provide common mysensors fixtures.""" +from __future__ import annotations + +from collections.abc import AsyncGenerator, Generator +import json +from typing import Any +from unittest.mock import MagicMock, patch + +from mysensors.persistence import MySensorsJSONDecoder +from mysensors.sensor import Sensor import pytest from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE +from homeassistant.components.mysensors.const import ( + CONF_BAUD_RATE, + CONF_DEVICE, + CONF_GATEWAY_TYPE, + CONF_GATEWAY_TYPE_SERIAL, + CONF_GATEWAYS, + DOMAIN, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(autouse=True) +def device_tracker_storage(mock_device_tracker_conf): + """Mock out device tracker known devices storage.""" + devices = mock_device_tracker_conf + return devices @pytest.fixture(name="mqtt") -async def mock_mqtt_fixture(hass): +def mock_mqtt_fixture(hass) -> None: """Mock the MQTT integration.""" hass.config.components.add(MQTT_DOMAIN) + + +@pytest.fixture(name="is_serial_port") +def is_serial_port_fixture() -> Generator[MagicMock, None, None]: + """Patch the serial port check.""" + with patch("homeassistant.components.mysensors.gateway.cv.isdevice") as is_device: + is_device.side_effect = lambda device: device + yield is_device + + +@pytest.fixture(name="gateway_nodes") +def gateway_nodes_fixture() -> dict[int, Sensor]: + """Return the gateway nodes dict.""" + return {} + + +@pytest.fixture(name="serial_transport") +async def serial_transport_fixture( + gateway_nodes: dict[int, Sensor], + is_serial_port: MagicMock, +) -> AsyncGenerator[dict[int, Sensor], None]: + """Mock a serial transport.""" + with patch( + "mysensors.gateway_serial.AsyncTransport", autospec=True + ) as transport_class, patch("mysensors.AsyncTasks", autospec=True) as tasks_class: + tasks = tasks_class.return_value + tasks.persistence = MagicMock + + mock_gateway_features(tasks, transport_class, gateway_nodes) + + yield transport_class + + +def mock_gateway_features( + tasks: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor] +) -> None: + """Mock the gateway features.""" + + async def mock_start_persistence(): + """Load nodes from via persistence.""" + gateway = transport_class.call_args[0][0] + gateway.sensors.update(nodes) + + tasks.start_persistence.side_effect = mock_start_persistence + + async def mock_start(): + """Mock the start method.""" + gateway = transport_class.call_args[0][0] + gateway.on_conn_made(gateway) + + tasks.start.side_effect = mock_start + + +@pytest.fixture(name="transport") +def transport_fixture(serial_transport: MagicMock) -> MagicMock: + """Return the default mocked transport.""" + return serial_transport + + +@pytest.fixture(name="serial_entry") +async def serial_entry_fixture(hass) -> MockConfigEntry: + """Create a config entry for a serial gateway.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + CONF_VERSION: "2.3", + CONF_DEVICE: "/test/device", + CONF_BAUD_RATE: DEFAULT_BAUD_RATE, + }, + ) + return entry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: + """Provide the config entry used for integration set up.""" + return serial_entry + + +@pytest.fixture +async def integration( + hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry +) -> AsyncGenerator[MockConfigEntry, None]: + """Set up the mysensors integration with a config entry.""" + device = config_entry.data[CONF_DEVICE] + config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} + config_entry.add_to_hass(hass) + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield config_entry + + +def load_nodes_state(fixture_path: str) -> dict: + """Load mysensors nodes fixture.""" + return json.loads(load_fixture(fixture_path), cls=MySensorsJSONDecoder) + + +def update_gateway_nodes( + gateway_nodes: dict[int, Sensor], nodes: dict[int, Sensor] +) -> dict: + """Update the gateway nodes.""" + gateway_nodes.update(nodes) + return nodes + + +@pytest.fixture(name="gps_sensor_state", scope="session") +def gps_sensor_state_fixture() -> dict: + """Load the gps sensor state.""" + return load_nodes_state("mysensors/gps_sensor_state.json") + + +@pytest.fixture +def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor: + """Load the gps sensor.""" + nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py new file mode 100644 index 00000000000..69caeac9977 --- /dev/null +++ b/tests/components/mysensors/test_sensor.py @@ -0,0 +1,10 @@ +"""Provide tests for mysensors sensor platform.""" + + +async def test_gps_sensor(hass, gps_sensor, integration): + """Test a gps sensor.""" + entity_id = "sensor.gps_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state.state == "40.741894,-73.989311,12" diff --git a/tests/fixtures/mysensors/gps_sensor_state.json b/tests/fixtures/mysensors/gps_sensor_state.json new file mode 100644 index 00000000000..654e30e7271 --- /dev/null +++ b/tests/fixtures/mysensors/gps_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 38, + "description": "", + "values": { + "49": "40.741894,-73.989311,12" + } + } + }, + "type": 17, + "sketch_name": "GPS Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} From a10847bef34b7e4005fb572ebb25a0ffe392e4c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Jun 2021 08:35:58 -1000 Subject: [PATCH 563/750] Fix isy994 fan when turn on is not called with a percentage (#49531) * Fix isy994 fan when turn on is not called with a percentage * Make insteon/fan.py logic --- homeassistant/components/isy994/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 73b5bd683ba..f9f3c6e5459 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -83,7 +83,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): **kwargs, ) -> None: """Send the turn on command to the ISY994 fan device.""" - await self.async_set_percentage(percentage) + await self.async_set_percentage(percentage or 67) async def async_turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 fan device.""" From a5362542addfa44ed756edf3bafa9e3b63ba10e0 Mon Sep 17 00:00:00 2001 From: Hristo Atanasov Date: Sun, 27 Jun 2021 21:55:36 +0300 Subject: [PATCH 564/750] Bulgarian language added in Google Translate TTS (#51985) * Added Bulgarian language Bulgarian language is supported by Google Translate TTS and by gTTS library. Tested all lockally and it works perfect. https://github.com/pndurette/gTTS * Bulgarian language added in v2.2.3 * Run script.gen_requirements_all Co-authored-by: Franck Nijhof --- homeassistant/components/google_translate/manifest.json | 2 +- homeassistant/components/google_translate/tts.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 890479f9ffd..b566f3447f4 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,7 +2,7 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS==2.2.2"], + "requirements": ["gTTS==2.2.3"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index c9a5eef2c83..9f6ca3a88b8 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_LANGUAGES = [ "af", "ar", + "bg", "bn", "bs", "ca", diff --git a/requirements_all.txt b/requirements_all.txt index 9921e527e04..4008823eef4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -641,7 +641,7 @@ freesms==0.2.0 fritzconnection==1.4.2 # homeassistant.components.google_translate -gTTS==2.2.2 +gTTS==2.2.3 # homeassistant.components.garages_amsterdam garages-amsterdam==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7093e842ce3..ecece3e9a72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ freebox-api==0.0.10 fritzconnection==1.4.2 # homeassistant.components.google_translate -gTTS==2.2.2 +gTTS==2.2.3 # homeassistant.components.garages_amsterdam garages-amsterdam==2.1.1 From da1d6d38212509ed0448f15bb8b8b5ce816f6a8d Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 27 Jun 2021 11:59:11 -0700 Subject: [PATCH 565/750] Add service to reset SmartTub reminders (#51824) * Add service to reset SmartTub reminders * add test Co-authored-by: Franck Nijhof --- .../components/smarttub/binary_sensor.py | 15 ++++++++++++ .../components/smarttub/services.yaml | 19 +++++++++++++++ .../components/smarttub/test_binary_sensor.py | 24 +++++++++++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 7bb4e8cc9e0..6ddeccadc74 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -30,6 +30,11 @@ ATTR_UPDATED_AT = "updated_at" # how many days to snooze the reminder for ATTR_REMINDER_DAYS = "days" +RESET_REMINDER_SCHEMA = { + vol.Required(ATTR_REMINDER_DAYS): vol.All( + vol.Coerce(int), vol.Range(min=30, max=365) + ) +} SNOOZE_REMINDER_SCHEMA = { vol.Required(ATTR_REMINDER_DAYS): vol.All( vol.Coerce(int), vol.Range(min=10, max=120) @@ -60,6 +65,11 @@ async def async_setup_entry(hass, entry, async_add_entities): SNOOZE_REMINDER_SCHEMA, "async_snooze", ) + platform.async_register_entity_service( + "reset_reminder", + RESET_REMINDER_SCHEMA, + "async_reset", + ) class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): @@ -127,6 +137,11 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): await self.reminder.snooze(days) await self.coordinator.async_request_refresh() + async def async_reset(self, days): + """Dismiss this reminder, and reset it to the specified number of days.""" + await self.reminder.reset(days) + await self.coordinator.async_request_refresh() + class SmartTubError(SmartTubEntity, BinarySensorEntity): """Indicates whether an error code is present. diff --git a/homeassistant/components/smarttub/services.yaml b/homeassistant/components/smarttub/services.yaml index bb5ee66f1d0..d9890dba35a 100644 --- a/homeassistant/components/smarttub/services.yaml +++ b/homeassistant/components/smarttub/services.yaml @@ -64,3 +64,22 @@ snooze_reminder: min: 10 max: 120 unit_of_measurement: days + +reset_reminder: + name: Reset a reminder + description: Reset a reminder, and set the next time it will be triggered. + target: + entity: + integration: smarttub + domain: binary_sensor + fields: + days: + name: Days + description: The number of days when the next reminder should trigger. + required: true + example: 180 + selector: + number: + min: 30 + max: 365 + unit_of_measurement: days diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index 98d404ef600..c84ef99328e 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -64,7 +64,7 @@ async def test_error(spa, hass, config_entry, mock_error): assert state.attributes["error_code"] == 11 -async def test_snooze(spa, setup_entry, hass): +async def test_snooze_reminder(spa, setup_entry, hass): """Test snoozing a reminder.""" entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder" @@ -76,9 +76,29 @@ async def test_snooze(spa, setup_entry, hass): "snooze_reminder", { "entity_id": entity_id, - "days": 30, + "days": days, }, blocking=True, ) reminder.snooze.assert_called_with(days) + + +async def test_reset_reminder(spa, setup_entry, hass): + """Test snoozing a reminder.""" + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder" + reminder = spa.get_reminders.return_value[0] + days = 180 + + await hass.services.async_call( + "smarttub", + "reset_reminder", + { + "entity_id": entity_id, + "days": days, + }, + blocking=True, + ) + + reminder.reset.assert_called_with(days) From 74aa428bd1b400c29813408b64dd559a81999f3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 27 Jun 2021 21:00:27 +0200 Subject: [PATCH 566/750] Implement color_mode support for ozw (#52063) --- homeassistant/components/ozw/light.py | 85 ++++++++++++++++++--------- tests/components/ozw/test_light.py | 42 +++++++++++-- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py index b5fffbcf34f..7c52da23fb4 100644 --- a/homeassistant/components/ozw/light.py +++ b/homeassistant/components/ozw/light.py @@ -5,14 +5,14 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, DOMAIN as LIGHT_DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.core import callback @@ -65,9 +65,11 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): super().__init__(values) self._color_channels = None self._hs = None - self._white = None + self._rgbw_color = None self._ct = None - self._supported_features = SUPPORT_BRIGHTNESS + self._attr_color_mode = None + self._attr_supported_features = 0 + self._attr_supported_color_modes = set() self._min_mireds = 153 # 6500K as a safe default self._max_mireds = 370 # 2700K as a safe default @@ -78,23 +80,29 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): def on_value_update(self): """Call when the underlying value(s) is added or updated.""" if self.values.dimming_duration is not None: - self._supported_features |= SUPPORT_TRANSITION - - if self.values.color is not None: - self._supported_features |= SUPPORT_COLOR + self._attr_supported_features |= SUPPORT_TRANSITION if self.values.color_channels is not None: # Support Color Temp if both white channels if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and ( self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE ): - self._supported_features |= SUPPORT_COLOR_TEMP + self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._attr_supported_color_modes.add(COLOR_MODE_HS) # Support White value if only a single white channel if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ ( (self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0 ): - self._supported_features |= SUPPORT_WHITE_VALUE + self._attr_supported_color_modes.add(COLOR_MODE_RGBW) + + if not self._attr_supported_color_modes and self.values.color is not None: + self._attr_supported_color_modes.add(COLOR_MODE_HS) + + if not self._attr_supported_color_modes: + self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + # Default: Brightness (no color) + self._attr_color_mode = COLOR_MODE_BRIGHTNESS if self.values.color is not None: self._calculate_color_values() @@ -116,20 +124,15 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): return self.values.target.value > 0 return self.values.primary.value > 0 - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - @property def hs_color(self): """Return the hs color.""" return self._hs @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white + def rgbw_color(self): + """Return the rgbw color.""" + return self._rgbw_color @property def color_temp(self): @@ -196,8 +199,8 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): self.async_set_duration(**kwargs) rgbw = None - white = kwargs.get(ATTR_WHITE_VALUE) hs_color = kwargs.get(ATTR_HS_COLOR) + rgbw_color = kwargs.get(ATTR_RGBW_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) if hs_color is not None: @@ -211,12 +214,16 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): rgbw += "00" # white LED must be off in order for color to work - elif white is not None: + elif rgbw_color is not None: + red = rgbw_color[0] + green = rgbw_color[1] + blue = rgbw_color[2] + white = rgbw_color[3] if self._color_channels & COLOR_CHANNEL_WARM_WHITE: # trim the CW value or it will not work correctly - rgbw = f"#000000{white:02x}" + rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}" else: - rgbw = f"#00000000{white:02x}" + rgbw = f"#{red:02x}{green:02x}{blue:02x}00{white:02x}" elif color_temp is not None: # Limit color temp to min/max values @@ -262,6 +269,9 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] self._hs = color_util.color_RGB_to_hs(*rgb) + # Light supports color, set color mode to hs + self._attr_color_mode = COLOR_MODE_HS + if self.values.color_channels is None: return @@ -286,15 +296,21 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): # Warm white if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._white = int(data[index : index + 2], 16) - temp_warm = self._white + white = int(data[index : index + 2], 16) + self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] + temp_warm = white + # Light supports rgbw, set color mode to rgbw + self._attr_color_mode = COLOR_MODE_RGBW index += 2 # Cold white if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._white = int(data[index : index + 2], 16) - temp_cold = self._white + white = int(data[index : index + 2], 16) + self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] + temp_cold = white + # Light supports rgbw, set color mode to rgbw + self._attr_color_mode = COLOR_MODE_RGBW # Calculate color temps based on white LED status if temp_cold or temp_warm: @@ -303,6 +319,17 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): - ((temp_cold / 255) * (self._max_mireds - self._min_mireds)) ) + if ( + self._color_channels & COLOR_CHANNEL_WARM_WHITE + and self._color_channels & COLOR_CHANNEL_COLD_WHITE + ): + # Light supports 5 channels, set color_mode to color_temp or hs + if rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0: + # Color channels turned off, set color mode to color_temp + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + else: + self._attr_color_mode = COLOR_MODE_HS + if not ( self._color_channels & COLOR_CHANNEL_RED or self._color_channels & COLOR_CHANNEL_GREEN diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py index 6388629be8c..a8ed4352f9a 100644 --- a/tests/components/ozw/test_light.py +++ b/tests/components/ozw/test_light.py @@ -1,4 +1,5 @@ """Test Z-Wave Lights.""" +from homeassistant.components.light import SUPPORT_TRANSITION from homeassistant.components.ozw.light import byte_to_zwave_brightness from .common import setup_ozw @@ -12,6 +13,8 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] # Test turning on # Beware that due to rounding, a roundtrip conversion does not always work @@ -51,6 +54,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "color_temp" # Test turning off new_transition = 6553 @@ -119,6 +123,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "color_temp" # Test set brightness to 0 new_brightness = 0 @@ -183,6 +188,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["color_mode"] == "hs" # Test setting hs_color new_color = [300, 70] @@ -216,6 +222,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["hs_color"] == (300.0, 70.196) + assert state.attributes["color_mode"] == "hs" # Test setting rgb_color new_color = [255, 154, 0] @@ -249,6 +256,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["rgb_color"] == (255, 153, 0) + assert state.attributes["color_mode"] == "hs" # Test setting xy_color new_color = [0.52, 0.43] @@ -282,6 +290,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["xy_color"] == (0.519, 0.429) + assert state.attributes["color_mode"] == "hs" # Test setting color temp new_color = 200 @@ -315,6 +324,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 200 + assert state.attributes["color_mode"] == "color_temp" # Test setting invalid color temp new_color = 120 @@ -348,6 +358,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 153 + assert state.attributes["color_mode"] == "color_temp" async def test_pure_rgb_dimmer_light( @@ -360,7 +371,9 @@ async def test_pure_rgb_dimmer_light( state = hass.states.get("light.kitchen_rgb_strip_level") assert state is not None assert state.state == "on" - assert state.attributes["supported_features"] == 17 + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["hs"] + assert state.attributes["color_mode"] == "hs" # Test setting hs_color new_color = [300, 70] @@ -390,6 +403,7 @@ async def test_pure_rgb_dimmer_light( assert state is not None assert state.state == "on" assert state.attributes["hs_color"] == (300.0, 70.196) + assert state.attributes["color_mode"] == "hs" async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): @@ -400,6 +414,8 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): state = hass.states.get("light.master_bedroom_l_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["brightness"] # Turn on the light new_brightness = 44 @@ -429,6 +445,7 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "brightness" async def test_no_ww_light( @@ -441,6 +458,8 @@ async def test_no_ww_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["rgbw"] # Turn on the light white_color = 190 @@ -449,7 +468,7 @@ async def test_no_ww_light( "turn_on", { "entity_id": "light.led_bulb_6_multi_colour_level", - "white_value": white_color, + "rgbw_color": [0, 0, 0, white_color], }, blocking=True, ) @@ -472,7 +491,8 @@ async def test_no_ww_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "on" - assert state.attributes["white_value"] == 190 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (0, 0, 0, 190) async def test_no_cw_light( @@ -485,6 +505,8 @@ async def test_no_cw_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["rgbw"] # Turn on the light white_color = 190 @@ -493,7 +515,7 @@ async def test_no_cw_light( "turn_on", { "entity_id": "light.led_bulb_6_multi_colour_level", - "white_value": white_color, + "rgbw_color": [0, 0, 0, white_color], }, blocking=True, ) @@ -516,7 +538,8 @@ async def test_no_cw_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "on" - assert state.attributes["white_value"] == 190 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (0, 0, 0, 190) async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_messages): @@ -527,6 +550,8 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] assert state.attributes["min_mireds"] == 153 assert state.attributes["max_mireds"] == 370 @@ -559,6 +584,7 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 190 + assert state.attributes["color_mode"] == "color_temp" async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages): @@ -569,6 +595,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] # Test turning on with new duration (newer openzwave) new_transition = 4180 @@ -597,6 +625,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) light_msg.encode() receive_message(light_msg) await hass.async_block_till_done() + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state.attributes["color_mode"] == "color_temp" # Test turning off with new duration (newer openzwave)(new max) await hass.services.async_call( @@ -649,3 +679,5 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) light_msg.encode() receive_message(light_msg) await hass.async_block_till_done() + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state.attributes["color_mode"] == "color_temp" From 23339cff950edd316ee59152b0d43770795db565 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:03:20 -0400 Subject: [PATCH 567/750] Add new climacell sensors (#52079) * Add new climacell sensors * lint * add new unit constants --- homeassistant/components/climacell/const.py | 112 ++++++++++++++++++- homeassistant/components/climacell/sensor.py | 29 +++-- homeassistant/const.py | 2 + tests/components/climacell/test_sensor.py | 81 ++++++++++---- tests/fixtures/climacell/v4.json | 8 +- 5 files changed, 197 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 5b62d05a78c..062de93375b 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -5,6 +5,7 @@ from pyclimacell.const import ( NOWCAST, HealthConcernType, PollenIndex, + PrecipitationType, PrimaryPollutantType, V3PollenIndex, WeatherCode, @@ -26,13 +27,29 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + IRRADIATION_WATTS_PER_SQUARE_METER, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) +from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert +from homeassistant.util.temperature import convert as temp_convert CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] @@ -58,6 +75,7 @@ ATTR_FIELD = "field" ATTR_METRIC_CONVERSION = "metric_conversion" ATTR_VALUE_MAP = "value_map" ATTR_IS_METRIC_CHECK = "is_metric_check" +ATTR_SCALE = "scale" # Additional attributes ATTR_WIND_GUST = "wind_gust" @@ -126,8 +144,94 @@ CC_ATTR_POLLEN_TREE = "treeIndex" CC_ATTR_POLLEN_WEED = "weedIndex" CC_ATTR_POLLEN_GRASS = "grassIndex" CC_ATTR_FIRE_INDEX = "fireIndex" +CC_ATTR_FEELS_LIKE = "temperatureApparent" +CC_ATTR_DEW_POINT = "dewPoint" +CC_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" +CC_ATTR_SOLAR_GHI = "solarGHI" +CC_ATTR_CLOUD_BASE = "cloudBase" +CC_ATTR_CLOUD_CEILING = "cloudCeiling" CC_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_ATTR_FEELS_LIKE, + ATTR_NAME: "Feels Like", + CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, + CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, + ATTR_METRIC_CONVERSION: lambda val: temp_convert( + val, TEMP_FAHRENHEIT, TEMP_CELSIUS + ), + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_DEW_POINT, + ATTR_NAME: "Dew Point", + CONF_UNIT_SYSTEM_IMPERIAL: TEMP_FAHRENHEIT, + CONF_UNIT_SYSTEM_METRIC: TEMP_CELSIUS, + ATTR_METRIC_CONVERSION: lambda val: temp_convert( + val, TEMP_FAHRENHEIT, TEMP_CELSIUS + ), + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_PRESSURE_SURFACE_LEVEL, + ATTR_NAME: "Pressure (Surface Level)", + CONF_UNIT_SYSTEM_IMPERIAL: PRESSURE_INHG, + CONF_UNIT_SYSTEM_METRIC: PRESSURE_HPA, + ATTR_METRIC_CONVERSION: lambda val: pressure_convert( + val, PRESSURE_INHG, PRESSURE_HPA + ), + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_SOLAR_GHI, + ATTR_NAME: "Global Horizontal Irradiance", + CONF_UNIT_SYSTEM_IMPERIAL: IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + CONF_UNIT_SYSTEM_METRIC: IRRADIATION_WATTS_PER_SQUARE_METER, + ATTR_METRIC_CONVERSION: 3.15459, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_CLOUD_BASE, + ATTR_NAME: "Cloud Base", + CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, + CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, + ATTR_METRIC_CONVERSION: lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_CLOUD_CEILING, + ATTR_NAME: "Cloud Ceiling", + CONF_UNIT_SYSTEM_IMPERIAL: LENGTH_MILES, + CONF_UNIT_SYSTEM_METRIC: LENGTH_KILOMETERS, + ATTR_METRIC_CONVERSION: lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_CLOUD_COVER, + ATTR_NAME: "Cloud Cover", + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_SCALE: 1 / 100, + }, + { + ATTR_FIELD: CC_ATTR_WIND_GUST, + ATTR_NAME: "Wind Gust", + CONF_UNIT_SYSTEM_IMPERIAL: SPEED_MILES_PER_HOUR, + CONF_UNIT_SYSTEM_METRIC: SPEED_METERS_PER_SECOND, + ATTR_METRIC_CONVERSION: lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_METERS + ) + / 3600, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_PRECIPITATION_TYPE, + ATTR_NAME: "Precipitation Type", + ATTR_VALUE_MAP: PrecipitationType, + }, { ATTR_FIELD: CC_ATTR_OZONE, ATTR_NAME: "Ozone", @@ -136,7 +240,7 @@ CC_SENSOR_TYPES = [ { ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, ATTR_NAME: "Particulate Matter < 2.5 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: True, @@ -144,7 +248,7 @@ CC_SENSOR_TYPES = [ { ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, ATTR_NAME: "Particulate Matter < 10 μm", - CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_IMPERIAL: CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: True, @@ -277,7 +381,7 @@ CC_V3_SENSOR_TYPES = [ ATTR_NAME: "Particulate Matter < 2.5 μm", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: False, }, { @@ -285,7 +389,7 @@ CC_V3_SENSOR_TYPES = [ ATTR_NAME: "Particulate Matter < 10 μm", CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, ATTR_IS_METRIC_CHECK: False, }, { diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index df611079403..2c620cc65a1 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -28,6 +28,7 @@ from .const import ( ATTR_FIELD, ATTR_IS_METRIC_CHECK, ATTR_METRIC_CONVERSION, + ATTR_SCALE, ATTR_VALUE_MAP, CC_SENSOR_TYPES, CC_V3_SENSOR_TYPES, @@ -103,9 +104,11 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type and CONF_UNIT_SYSTEM_METRIC in self.sensor_type ): - if self.hass.config.units.is_metric: - return self.sensor_type[CONF_UNIT_SYSTEM_METRIC] - return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + return ( + self.sensor_type[CONF_UNIT_SYSTEM_METRIC] + if self.hass.config.units.is_metric + else self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + ) return None @@ -117,8 +120,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): @property def state(self) -> str | int | float | None: """Return the state.""" + state = self._state + if state and ATTR_SCALE in self.sensor_type: + state *= self.sensor_type[ATTR_SCALE] + if ( - self._state is not None + state is not None and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type and CONF_UNIT_SYSTEM_METRIC in self.sensor_type and ATTR_METRIC_CONVERSION in self.sensor_type @@ -126,11 +133,17 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): and self.hass.config.units.is_metric == self.sensor_type[ATTR_IS_METRIC_CHECK] ): - return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) + conversion = self.sensor_type[ATTR_METRIC_CONVERSION] + # When conversion is a callable, we assume it's a single input function + if callable(conversion): + return round(conversion(state), 4) - if ATTR_VALUE_MAP in self.sensor_type and self._state is not None: - return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() - return self._state + return round(state * conversion, 4) + + if ATTR_VALUE_MAP in self.sensor_type and state is not None: + return self.sensor_type[ATTR_VALUE_MAP](state).name.lower() + + return state class ClimaCellSensorEntity(BaseClimaCellSensorEntity): diff --git a/homeassistant/const.py b/homeassistant/const.py index a25a5ec0908..f5308148823 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -494,6 +494,7 @@ PERCENTAGE: Final = "%" # Irradiation units IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" +IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)" # Precipitation units PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" @@ -501,6 +502,7 @@ PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" # Concentration units CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" +CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 653a989c4b7..d93bdb5fae8 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -42,6 +42,49 @@ FIRE_INDEX = "fire_index" GRASS_POLLEN = "grass_pollen_index" WEED_POLLEN = "weed_pollen_index" TREE_POLLEN = "tree_pollen_index" +FEELS_LIKE = "feels_like" +DEW_POINT = "dew_point" +PRESSURE_SURFACE_LEVEL = "pressure_surface_level" +SNOW_ACCUMULATION = "snow_accumulation" +ICE_ACCUMULATION = "ice_accumulation" +GHI = "global_horizontal_irradiance" +CLOUD_BASE = "cloud_base" +CLOUD_COVER = "cloud_cover" +CLOUD_CEILING = "cloud_ceiling" +WIND_GUST = "wind_gust" +PRECIPITATION_TYPE = "precipitation_type" + +V3_FIELDS = [ + O3, + CO, + NO2, + SO2, + PM25, + PM10, + MEP_AQI, + MEP_HEALTH_CONCERN, + MEP_PRIMARY_POLLUTANT, + EPA_AQI, + EPA_HEALTH_CONCERN, + EPA_PRIMARY_POLLUTANT, + FIRE_INDEX, + GRASS_POLLEN, + WEED_POLLEN, + TREE_POLLEN, +] + +V4_FIELDS = [ + *V3_FIELDS, + FEELS_LIKE, + DEW_POINT, + PRESSURE_SURFACE_LEVEL, + GHI, + CLOUD_BASE, + CLOUD_COVER, + CLOUD_CEILING, + WIND_GUST, + PRECIPITATION_TYPE, +] @callback @@ -56,7 +99,9 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: assert updated_entry.disabled is False -async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: +async def _setup( + hass: HomeAssistant, sensors: list[str], config: dict[str, Any] +) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", @@ -72,27 +117,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - for entity_name in ( - O3, - CO, - NO2, - SO2, - PM25, - PM10, - MEP_AQI, - MEP_HEALTH_CONCERN, - MEP_PRIMARY_POLLUTANT, - EPA_AQI, - EPA_HEALTH_CONCERN, - EPA_PRIMARY_POLLUTANT, - FIRE_INDEX, - GRASS_POLLEN, - WEED_POLLEN, - TREE_POLLEN, - ): + for entity_name in sensors: _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 16 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == len(sensors) def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): @@ -108,7 +136,7 @@ async def test_v3_sensor( climacell_config_entry_update: pytest.fixture, ) -> None: """Test v3 sensor data.""" - await _setup(hass, API_V3_ENTRY_DATA) + await _setup(hass, V3_FIELDS, API_V3_ENTRY_DATA) check_sensor_state(hass, O3, "52.625") check_sensor_state(hass, CO, "0.875") check_sensor_state(hass, NO2, "14.1875") @@ -132,7 +160,7 @@ async def test_v4_sensor( climacell_config_entry_update: pytest.fixture, ) -> None: """Test v4 sensor data.""" - await _setup(hass, API_V4_ENTRY_DATA) + await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) check_sensor_state(hass, O3, "46.53") check_sensor_state(hass, CO, "0.63") check_sensor_state(hass, NO2, "10.67") @@ -149,3 +177,12 @@ async def test_v4_sensor( check_sensor_state(hass, GRASS_POLLEN, "none") check_sensor_state(hass, WEED_POLLEN, "none") check_sensor_state(hass, TREE_POLLEN, "none") + check_sensor_state(hass, FEELS_LIKE, "38.5") + check_sensor_state(hass, DEW_POINT, "22.6778") + check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688") + check_sensor_state(hass, GHI, "0.0") + check_sensor_state(hass, CLOUD_BASE, "1.1909") + check_sensor_state(hass, CLOUD_COVER, "1.0") + check_sensor_state(hass, CLOUD_CEILING, "1.1909") + check_sensor_state(hass, WIND_GUST, "5.6506") + check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/fixtures/climacell/v4.json b/tests/fixtures/climacell/v4.json index f2f10b0360e..02f76ab7d27 100644 --- a/tests/fixtures/climacell/v4.json +++ b/tests/fixtures/climacell/v4.json @@ -25,7 +25,13 @@ "treeIndex": 0, "weedIndex": 0, "grassIndex": 0, - "fireIndex": 10 + "fireIndex": 10, + "temperatureApparent": 101.3, + "dewPoint": 72.82, + "pressureSurfaceLevel": 29.47, + "solarGHI": 0, + "cloudBase": 0.74, + "cloudCeiling": 0.74 }, "forecasts": { "nowcast": [ From 2d1744c573af7e2275436a992acef53207f3b05f Mon Sep 17 00:00:00 2001 From: avee87 Date: Sun, 27 Jun 2021 20:04:42 +0100 Subject: [PATCH 568/750] Add forecasts to MetOffice integration (#50876) * MetOfficeData now retrieves both 3-hourly and daily data (full forecast data, as well as "now" snapshot) on each update * Bump datapoint API up to latest version * Create 2 sets of sensors - one of each set for 3-hourly and for daily data (same ones initially enabled, for now) * Create two entities (one each for 3-hourly and daily data) and also add in the forecast data for each dataset * Testing changes to accommodate now having two sets of everything for 3-hourly and daily update data * Removed unused import (reported by flake8) * As per conversation with @MatthewFlamm leave the 3-hourly entity's unique_id unchanged (although the display name is changed) * Make some improvements based on reviews Make some improvements and fix up the formatting/linting failures. * Make some improvements based on reviews Make some improvements and fix up the formatting/linting failures. * Added more test coverage * import asyncio * Try to fix test * Rewrote everything using CoordinatorEntity * Fixed config flow * Fixed lint errors Co-authored-by: MrHarcombe Co-authored-by: Henco Appel --- .../components/metoffice/__init__.py | 56 +++- .../components/metoffice/config_flow.py | 15 +- homeassistant/components/metoffice/const.py | 8 +- homeassistant/components/metoffice/data.py | 77 +----- homeassistant/components/metoffice/helpers.py | 44 +++ .../components/metoffice/manifest.json | 2 +- homeassistant/components/metoffice/sensor.py | 102 ++++--- homeassistant/components/metoffice/weather.py | 164 +++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/metoffice/test_config_flow.py | 4 + tests/components/metoffice/test_sensor.py | 13 + tests/components/metoffice/test_weather.py | 152 ++++++++++- tests/fixtures/metoffice.json | 254 ++++++++++++++++++ 14 files changed, 659 insertions(+), 236 deletions(-) create mode 100644 homeassistant/components/metoffice/helpers.py diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index f73c87f94ec..1a55c940d81 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,7 +1,10 @@ """The Met Office integration.""" +import asyncio import logging +import datapoint + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant @@ -11,11 +14,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, - METOFFICE_COORDINATOR, - METOFFICE_DATA, + METOFFICE_COORDINATES, + METOFFICE_DAILY_COORDINATOR, + METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, + MODE_3HOURLY, + MODE_DAILY, ) from .data import MetOfficeData +from .helpers import fetch_data, fetch_site _LOGGER = logging.getLogger(__name__) @@ -30,30 +37,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key = entry.data[CONF_API_KEY] site_name = entry.data[CONF_NAME] - metoffice_data = MetOfficeData(hass, api_key, latitude, longitude) - await metoffice_data.async_update_site() - if metoffice_data.site_name is None: + connection = datapoint.connection(api_key=api_key) + + site = await hass.async_add_executor_job( + fetch_site, connection, latitude, longitude + ) + if site is None: raise ConfigEntryNotReady() - metoffice_coordinator = DataUpdateCoordinator( + async def async_update_3hourly() -> MetOfficeData: + return await hass.async_add_executor_job( + fetch_data, connection, site, MODE_3HOURLY + ) + + async def async_update_daily() -> MetOfficeData: + return await hass.async_add_executor_job( + fetch_data, connection, site, MODE_DAILY + ) + + metoffice_hourly_coordinator = DataUpdateCoordinator( hass, _LOGGER, - name=f"MetOffice Coordinator for {site_name}", - update_method=metoffice_data.async_update, + name=f"MetOffice Hourly Coordinator for {site_name}", + update_method=async_update_3hourly, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + + metoffice_daily_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"MetOffice Daily Coordinator for {site_name}", + update_method=async_update_daily, update_interval=DEFAULT_SCAN_INTERVAL, ) metoffice_hass_data = hass.data.setdefault(DOMAIN, {}) metoffice_hass_data[entry.entry_id] = { - METOFFICE_DATA: metoffice_data, - METOFFICE_COORDINATOR: metoffice_coordinator, + METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator, + METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator, METOFFICE_NAME: site_name, + METOFFICE_COORDINATES: f"{latitude}_{longitude}", } # Fetch initial data so we have data when entities subscribe - await metoffice_coordinator.async_refresh() - if metoffice_data.now is None: - raise ConfigEntryNotReady() + await asyncio.gather( + metoffice_hourly_coordinator.async_config_entry_first_refresh(), + metoffice_daily_coordinator.async_config_entry_first_refresh(), + ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 071106deff9..375eaa1ec1b 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,14 +1,15 @@ """Config flow for Met Office integration.""" import logging +import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components.metoffice.helpers import fetch_site from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers import config_validation as cv from .const import DOMAIN -from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) @@ -22,12 +23,16 @@ async def validate_input(hass: core.HomeAssistant, data): longitude = data[CONF_LONGITUDE] api_key = data[CONF_API_KEY] - metoffice_data = MetOfficeData(hass, api_key, latitude, longitude) - await metoffice_data.async_update_site() - if metoffice_data.site_name is None: + connection = datapoint.connection(api_key=api_key) + + site = await hass.async_add_executor_job( + fetch_site, connection, latitude, longitude + ) + + if site is None: raise CannotConnect() - return {"site_name": metoffice_data.site_name} + return {"site_name": site.name} class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e710911ee59..0b275f301cd 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -25,12 +25,16 @@ ATTRIBUTION = "Data provided by the Met Office" DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) -METOFFICE_DATA = "metoffice_data" -METOFFICE_COORDINATOR = "metoffice_coordinator" +METOFFICE_COORDINATES = "metoffice_coordinates" +METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator" +METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator" METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_NAME = "metoffice_name" MODE_3HOURLY = "3hourly" +MODE_3HOURLY_LABEL = "3-Hourly" +MODE_DAILY = "daily" +MODE_DAILY_LABEL = "Daily" CONDITION_CLASSES = { ATTR_CONDITION_CLOUDY: ["7", "8"], diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 8f718b8d4b8..607c09e90b6 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,78 +1,11 @@ """Common Met Office Data class used by both sensor and entity.""" -import logging - -import datapoint - -from .const import MODE_3HOURLY - -_LOGGER = logging.getLogger(__name__) - class MetOfficeData: - """Get current and forecast data from Datapoint. + """Data structure for MetOffice weather and forecast.""" - Please note that the 'datapoint' library is not asyncio-friendly, so some - calls have had to be wrapped with the standard hassio helper - async_add_executor_job. - """ - - def __init__(self, hass, api_key, latitude, longitude): + def __init__(self, now, forecast, site): """Initialize the data object.""" - self._hass = hass - self._datapoint = datapoint.connection(api_key=api_key) - self._site = None - - # Public attributes - self.latitude = latitude - self.longitude = longitude - - # Holds the current data from the Met Office - self.site_id = None - self.site_name = None - self.now = None - - async def async_update_site(self): - """Async wrapper for getting the DataPoint site.""" - return await self._hass.async_add_executor_job(self._update_site) - - def _update_site(self): - """Return the nearest DataPoint Site to the held latitude/longitude.""" - try: - new_site = self._datapoint.get_nearest_forecast_site( - latitude=self.latitude, longitude=self.longitude - ) - if self._site is None or self._site.id != new_site.id: - self._site = new_site - self.now = None - - self.site_id = self._site.id - self.site_name = self._site.name - - except datapoint.exceptions.APIException as err: - _LOGGER.error("Received error from Met Office Datapoint: %s", err) - self._site = None - self.site_id = None - self.site_name = None - self.now = None - - return self._site - - async def async_update(self): - """Async wrapper for update method.""" - return await self._hass.async_add_executor_job(self._update) - - def _update(self): - """Get the latest data from DataPoint.""" - if self._site is None: - _LOGGER.error("No Met Office forecast site held, check logs for problems") - return - - try: - forecast = self._datapoint.get_forecast_for_site( - self._site.id, MODE_3HOURLY - ) - self.now = forecast.now() - except (ValueError, datapoint.exceptions.APIException) as err: - _LOGGER.error("Check Met Office connection: %s", err.args) - self.now = None + self.now = now + self.forecast = forecast + self.site = site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py new file mode 100644 index 00000000000..e7518f86b5b --- /dev/null +++ b/homeassistant/components/metoffice/helpers.py @@ -0,0 +1,44 @@ +"""Helpers used for Met Office integration.""" + +import logging + +import datapoint + +from homeassistant.helpers.update_coordinator import UpdateFailed +from homeassistant.util import utcnow + +from .data import MetOfficeData + +_LOGGER = logging.getLogger(__name__) + + +def fetch_site(connection: datapoint.Manager, latitude, longitude): + """Fetch site information from Datapoint API.""" + try: + return connection.get_nearest_forecast_site( + latitude=latitude, longitude=longitude + ) + except datapoint.exceptions.APIException as err: + _LOGGER.error("Received error from Met Office Datapoint: %s", err) + return None + + +def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: + """Fetch weather and forecast from Datapoint API.""" + try: + forecast = connection.get_forecast_for_site(site.id, mode) + except (ValueError, datapoint.exceptions.APIException) as err: + _LOGGER.error("Check Met Office connection: %s", err.args) + raise UpdateFailed from err + else: + time_now = utcnow() + return MetOfficeData( + forecast.now(), + [ + timestep + for day in forecast.days + for timestep in day.timesteps + if timestep.date > time_now + ], + site, + ) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 31a768eee8d..db6832b04b4 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -2,7 +2,7 @@ "domain": "metoffice", "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", - "requirements": ["datapoint==0.9.5"], + "requirements": ["datapoint==0.9.8"], "codeowners": ["@MrHarcombe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index a437ecd1fea..6b45dac22e7 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -10,16 +10,21 @@ from homeassistant.const import ( TEMP_CELSIUS, UV_INDEX, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTRIBUTION, CONDITION_CLASSES, DOMAIN, - METOFFICE_COORDINATOR, - METOFFICE_DATA, + METOFFICE_COORDINATES, + METOFFICE_DAILY_COORDINATOR, + METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, + MODE_3HOURLY_LABEL, + MODE_DAILY, + MODE_DAILY_LABEL, VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) @@ -85,28 +90,40 @@ async def async_setup_entry( async_add_entities( [ - MetOfficeCurrentSensor(entry.data, hass_data, sensor_type) + MetOfficeCurrentSensor( + hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True, sensor_type + ) + for sensor_type in SENSOR_TYPES + ] + + [ + MetOfficeCurrentSensor( + hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False, sensor_type + ) for sensor_type in SENSOR_TYPES ], False, ) -class MetOfficeCurrentSensor(SensorEntity): +class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): """Implementation of a Met Office current weather condition sensor.""" - def __init__(self, entry_data, hass_data, sensor_type): + def __init__(self, coordinator, hass_data, use_3hourly, sensor_type): """Initialize the sensor.""" - self._data = hass_data[METOFFICE_DATA] - self._coordinator = hass_data[METOFFICE_COORDINATOR] + super().__init__(coordinator) self._type = sensor_type - self._name = f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]}" - self._unique_id = f"{SENSOR_TYPES[self._type][0]}_{self._data.latitude}_{self._data.longitude}" + mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._name = ( + f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]} {mode_label}" + ) + self._unique_id = ( + f"{SENSOR_TYPES[self._type][0]}_{hass_data[METOFFICE_COORDINATES]}" + ) + if not use_3hourly: + self._unique_id = f"{self._unique_id}_{MODE_DAILY}" - self.metoffice_site_id = None - self.metoffice_site_name = None - self.metoffice_now = None + self.use_3hourly = use_3hourly @property def name(self): @@ -124,22 +141,26 @@ class MetOfficeCurrentSensor(SensorEntity): value = None if self._type == "visibility_distance" and hasattr( - self.metoffice_now, "visibility" + self.coordinator.data.now, "visibility" ): - value = VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value) + value = VISIBILITY_DISTANCE_CLASSES.get( + self.coordinator.data.now.visibility.value + ) - if self._type == "visibility" and hasattr(self.metoffice_now, "visibility"): - value = VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value) + if self._type == "visibility" and hasattr( + self.coordinator.data.now, "visibility" + ): + value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value) - elif self._type == "weather" and hasattr(self.metoffice_now, self._type): + elif self._type == "weather" and hasattr(self.coordinator.data.now, self._type): value = [ k for k, v in CONDITION_CLASSES.items() - if self.metoffice_now.weather.value in v + if self.coordinator.data.now.weather.value in v ][0] - elif hasattr(self.metoffice_now, self._type): - value = getattr(self.metoffice_now, self._type) + elif hasattr(self.coordinator.data.now, self._type): + value = getattr(self.coordinator.data.now, self._type) if not isinstance(value, int): value = value.value @@ -175,44 +196,13 @@ class MetOfficeCurrentSensor(SensorEntity): """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_LAST_UPDATE: self.metoffice_now.date if self.metoffice_now else None, + ATTR_LAST_UPDATE: self.coordinator.data.now.date, ATTR_SENSOR_ID: self._type, - ATTR_SITE_ID: self.metoffice_site_id if self.metoffice_site_id else None, - ATTR_SITE_NAME: self.metoffice_site_name - if self.metoffice_site_name - else None, + ATTR_SITE_ID: self.coordinator.data.site.id, + ATTR_SITE_NAME: self.coordinator.data.site.name, } - async def async_added_to_hass(self) -> None: - """Set up a listener and load data.""" - self.async_on_remove( - self._coordinator.async_add_listener(self._update_callback) - ) - self._update_callback() - - async def async_update(self): - """Schedule a custom update via the common entity update service.""" - await self._coordinator.async_request_refresh() - - @callback - def _update_callback(self) -> None: - """Load data from integration.""" - self.metoffice_site_id = self._data.site_id - self.metoffice_site_name = self._data.site_name - self.metoffice_now = self._data.now - self.async_write_ha_state() - - @property - def should_poll(self) -> bool: - """Entities do not individually poll.""" - return False - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return SENSOR_TYPES[self._type][4] - - @property - def available(self): - """Return if state is available.""" - return self.metoffice_site_id is not None and self.metoffice_now is not None + return SENSOR_TYPES[self._type][4] and self.use_3hourly diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 5962300bb85..0b1933c665f 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,17 +1,30 @@ """Support for UK Met Office weather service.""" -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTRIBUTION, CONDITION_CLASSES, DEFAULT_NAME, DOMAIN, - METOFFICE_COORDINATOR, - METOFFICE_DATA, + METOFFICE_COORDINATES, + METOFFICE_DAILY_COORDINATOR, + METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, + MODE_3HOURLY_LABEL, + MODE_DAILY, + MODE_DAILY_LABEL, VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) @@ -25,27 +38,48 @@ async def async_setup_entry( async_add_entities( [ - MetOfficeWeather( - entry.data, - hass_data, - ) + MetOfficeWeather(hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, True), + MetOfficeWeather(hass_data[METOFFICE_DAILY_COORDINATOR], hass_data, False), ], False, ) -class MetOfficeWeather(WeatherEntity): +def _build_forecast_data(timestep): + data = {} + data[ATTR_FORECAST_TIME] = timestep.date + if timestep.weather: + data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) + if timestep.precipitation: + data[ATTR_FORECAST_PRECIPITATION] = timestep.precipitation.value + if timestep.temperature: + data[ATTR_FORECAST_TEMP] = timestep.temperature.value + if timestep.wind_direction: + data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value + if timestep.wind_speed: + data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + return data + + +def _get_weather_condition(metoffice_code): + for hass_name, metoffice_codes in CONDITION_CLASSES.items(): + if metoffice_code in metoffice_codes: + return hass_name + return None + + +class MetOfficeWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Office weather condition.""" - def __init__(self, entry_data, hass_data): + def __init__(self, coordinator, hass_data, use_3hourly): """Initialise the platform with a data instance.""" - self._data = hass_data[METOFFICE_DATA] - self._coordinator = hass_data[METOFFICE_COORDINATOR] + super().__init__(coordinator) - self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]}" - self._unique_id = f"{self._data.latitude}_{self._data.longitude}" - - self.metoffice_now = None + mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" + self._unique_id = hass_data[METOFFICE_COORDINATES] + if not use_3hourly: + self._unique_id = f"{self._unique_id}_{MODE_DAILY}" @property def name(self): @@ -60,24 +94,16 @@ class MetOfficeWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return ( - [ - k - for k, v in CONDITION_CLASSES.items() - if self.metoffice_now.weather.value in v - ][0] - if self.metoffice_now - else None - ) + if self.coordinator.data.now: + return _get_weather_condition(self.coordinator.data.now.weather.value) + return None @property def temperature(self): """Return the platform temperature.""" - return ( - self.metoffice_now.temperature.value - if self.metoffice_now and self.metoffice_now.temperature - else None - ) + if self.coordinator.data.now.temperature: + return self.coordinator.data.now.temperature.value + return None @property def temperature_unit(self): @@ -88,8 +114,13 @@ class MetOfficeWeather(WeatherEntity): def visibility(self): """Return the platform visibility.""" _visibility = None - if hasattr(self.metoffice_now, "visibility"): - _visibility = f"{VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)} - {VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)}" + weather_now = self.coordinator.data.now + if hasattr(weather_now, "visibility"): + visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value) + visibility_distance = VISIBILITY_DISTANCE_CLASSES.get( + weather_now.visibility.value + ) + _visibility = f"{visibility_class} - {visibility_distance}" return _visibility @property @@ -100,63 +131,46 @@ class MetOfficeWeather(WeatherEntity): @property def pressure(self): """Return the mean sea-level pressure.""" - return ( - self.metoffice_now.pressure.value - if self.metoffice_now and self.metoffice_now.pressure - else None - ) + weather_now = self.coordinator.data.now + if weather_now and weather_now.pressure: + return weather_now.pressure.value + return None @property def humidity(self): """Return the relative humidity.""" - return ( - self.metoffice_now.humidity.value - if self.metoffice_now and self.metoffice_now.humidity - else None - ) + weather_now = self.coordinator.data.now + if weather_now and weather_now.humidity: + return weather_now.humidity.value + return None @property def wind_speed(self): """Return the wind speed.""" - return ( - self.metoffice_now.wind_speed.value - if self.metoffice_now and self.metoffice_now.wind_speed - else None - ) + weather_now = self.coordinator.data.now + if weather_now and weather_now.wind_speed: + return weather_now.wind_speed.value + return None @property def wind_bearing(self): """Return the wind bearing.""" - return ( - self.metoffice_now.wind_direction.value - if self.metoffice_now and self.metoffice_now.wind_direction - else None - ) + weather_now = self.coordinator.data.now + if weather_now and weather_now.wind_direction: + return weather_now.wind_direction.value + return None + + @property + def forecast(self): + """Return the forecast array.""" + if self.coordinator.data.forecast is None: + return None + return [ + _build_forecast_data(timestep) + for timestep in self.coordinator.data.forecast + ] @property def attribution(self): """Return the attribution.""" return ATTRIBUTION - - async def async_added_to_hass(self) -> None: - """Set up a listener and load data.""" - self.async_on_remove( - self._coordinator.async_add_listener(self._update_callback) - ) - self._update_callback() - - @callback - def _update_callback(self) -> None: - """Load data from integration.""" - self.metoffice_now = self._data.now - self.async_write_ha_state() - - @property - def should_poll(self) -> bool: - """Entities do not individually poll.""" - return False - - @property - def available(self): - """Return if state is available.""" - return self.metoffice_now is not None diff --git a/requirements_all.txt b/requirements_all.txt index 4008823eef4..c261d2b478d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -473,7 +473,7 @@ coronavirus==1.1.1 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.9.5 +datapoint==0.9.8 # homeassistant.components.debugpy debugpy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecece3e9a72..cc9b9ac9736 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ coronavirus==1.1.1 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.9.5 +datapoint==0.9.8 # homeassistant.components.debugpy debugpy==1.3.0 diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 8f01f4b9643..55dd54d1c87 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -68,6 +68,10 @@ async def test_form_already_configured(hass, requests_mock): "/public/data/val/wxfcs/all/json/354107?res=3hourly", text="", ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", + text="", + ) MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 43f460056f9..e603d0f93f6 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -29,12 +29,17 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly, ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", + text=wavertree_daily, + ) entry = MockConfigEntry( domain=DOMAIN, @@ -72,15 +77,23 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"]) + kingslynn_daily = json.dumps(mock_json["kingslynn_daily"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily + ) requests_mock.get( "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily + ) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 18edbc4a972..6d4187c7023 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -9,6 +9,7 @@ from homeassistant.util import utcnow from . import NewDateTime from .const import ( + DATETIME_FORMAT, METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, WAVERTREE_SENSOR_RESULTS, @@ -26,6 +27,7 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="") requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") + requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="") entry = MockConfigEntry( domain=DOMAIN, @@ -35,9 +37,10 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("weather.met_office_wavertree") is None + assert hass.states.get("weather.met_office_wavertree_3hourly") is None + assert hass.states.get("weather.met_office_wavertree_daily") is None for sensor_id in WAVERTREE_SENSOR_RESULTS: - sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] + sensor_name, _ = WAVERTREE_SENSOR_RESULTS[sensor_id] sensor = hass.states.get(f"sensor.wavertree_{sensor_name}") assert sensor is None @@ -53,11 +56,15 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily + ) entry = MockConfigEntry( domain=DOMAIN, @@ -67,16 +74,23 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - entity = hass.states.get("weather.met_office_wavertree") + entity = hass.states.get("weather.met_office_wavertree_3_hourly") + assert entity + + entity = hass.states.get("weather.met_office_wavertree_daily") assert entity requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") + requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="") future_time = utcnow() + timedelta(minutes=20) async_fire_time_changed(hass, future_time) await hass.async_block_till_done() - entity = hass.states.get("weather.met_office_wavertree") + entity = hass.states.get("weather.met_office_wavertree_3_hourly") + assert entity.state == STATE_UNAVAILABLE + + entity = hass.states.get("weather.met_office_wavertree_daily") assert entity.state == STATE_UNAVAILABLE @@ -91,12 +105,17 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly, ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", + text=wavertree_daily, + ) entry = MockConfigEntry( domain=DOMAIN, @@ -106,8 +125,8 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - # Wavertree weather platform expected results - entity = hass.states.get("weather.met_office_wavertree") + # Wavertree 3-hourly weather platform expected results + entity = hass.states.get("weather.met_office_wavertree_3_hourly") assert entity assert entity.state == "sunny" @@ -117,6 +136,41 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti assert entity.attributes.get("visibility") == "Good - 10-20" assert entity.attributes.get("humidity") == 50 + # Forecasts added - just pick out 1 entry to check + assert len(entity.attributes.get("forecast")) == 35 + + assert ( + entity.attributes.get("forecast")[26]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-28 21:00:00+0000" + ) + assert entity.attributes.get("forecast")[26]["condition"] == "cloudy" + assert entity.attributes.get("forecast")[26]["temperature"] == 10 + assert entity.attributes.get("forecast")[26]["wind_speed"] == 4 + assert entity.attributes.get("forecast")[26]["wind_bearing"] == "NNE" + + # Wavertree daily weather platform expected results + entity = hass.states.get("weather.met_office_wavertree_daily") + assert entity + + assert entity.state == "sunny" + assert entity.attributes.get("temperature") == 19 + assert entity.attributes.get("wind_speed") == 9 + assert entity.attributes.get("wind_bearing") == "SSE" + assert entity.attributes.get("visibility") == "Good - 10-20" + assert entity.attributes.get("humidity") == 50 + + # Also has Forecasts added - again, just pick out 1 entry to check + assert len(entity.attributes.get("forecast")) == 8 + + assert ( + entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-29 12:00:00+0000" + ) + assert entity.attributes.get("forecast")[7]["condition"] == "rainy" + assert entity.attributes.get("forecast")[7]["temperature"] == 13 + assert entity.attributes.get("forecast")[7]["wind_speed"] == 13 + assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE" + @patch( "datapoint.Forecast.datetime.datetime", @@ -129,15 +183,23 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t mock_json = json.loads(load_fixture("metoffice.json")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"]) + kingslynn_daily = json.dumps(mock_json["kingslynn_daily"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily + ) requests_mock.get( "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly ) + requests_mock.get( + "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily + ) entry = MockConfigEntry( domain=DOMAIN, @@ -153,8 +215,8 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() - # Wavertree weather platform expected results - entity = hass.states.get("weather.met_office_wavertree") + # Wavertree 3-hourly weather platform expected results + entity = hass.states.get("weather.met_office_wavertree_3_hourly") assert entity assert entity.state == "sunny" @@ -164,8 +226,43 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert entity.attributes.get("visibility") == "Good - 10-20" assert entity.attributes.get("humidity") == 50 - # King's Lynn weather platform expected results - entity = hass.states.get("weather.met_office_king_s_lynn") + # Forecasts added - just pick out 1 entry to check + assert len(entity.attributes.get("forecast")) == 35 + + assert ( + entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-27 21:00:00+0000" + ) + assert entity.attributes.get("forecast")[18]["condition"] == "sunny" + assert entity.attributes.get("forecast")[18]["temperature"] == 9 + assert entity.attributes.get("forecast")[18]["wind_speed"] == 4 + assert entity.attributes.get("forecast")[18]["wind_bearing"] == "NW" + + # Wavertree daily weather platform expected results + entity = hass.states.get("weather.met_office_wavertree_daily") + assert entity + + assert entity.state == "sunny" + assert entity.attributes.get("temperature") == 19 + assert entity.attributes.get("wind_speed") == 9 + assert entity.attributes.get("wind_bearing") == "SSE" + assert entity.attributes.get("visibility") == "Good - 10-20" + assert entity.attributes.get("humidity") == 50 + + # Also has Forecasts added - again, just pick out 1 entry to check + assert len(entity.attributes.get("forecast")) == 8 + + assert ( + entity.attributes.get("forecast")[7]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-29 12:00:00+0000" + ) + assert entity.attributes.get("forecast")[7]["condition"] == "rainy" + assert entity.attributes.get("forecast")[7]["temperature"] == 13 + assert entity.attributes.get("forecast")[7]["wind_speed"] == 13 + assert entity.attributes.get("forecast")[7]["wind_bearing"] == "SE" + + # King's Lynn 3-hourly weather platform expected results + entity = hass.states.get("weather.met_office_king_s_lynn_3_hourly") assert entity assert entity.state == "sunny" @@ -174,3 +271,38 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t assert entity.attributes.get("wind_bearing") == "E" assert entity.attributes.get("visibility") == "Very Good - 20-40" assert entity.attributes.get("humidity") == 60 + + # Also has Forecast added - just pick out 1 entry to check + assert len(entity.attributes.get("forecast")) == 35 + + assert ( + entity.attributes.get("forecast")[18]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-27 21:00:00+0000" + ) + assert entity.attributes.get("forecast")[18]["condition"] == "cloudy" + assert entity.attributes.get("forecast")[18]["temperature"] == 10 + assert entity.attributes.get("forecast")[18]["wind_speed"] == 7 + assert entity.attributes.get("forecast")[18]["wind_bearing"] == "SE" + + # King's Lynn daily weather platform expected results + entity = hass.states.get("weather.met_office_king_s_lynn_daily") + assert entity + + assert entity.state == "cloudy" + assert entity.attributes.get("temperature") == 9 + assert entity.attributes.get("wind_speed") == 4 + assert entity.attributes.get("wind_bearing") == "ESE" + assert entity.attributes.get("visibility") == "Very Good - 20-40" + assert entity.attributes.get("humidity") == 75 + + # All should have Forecast added - again, just picking out 1 entry to check + assert len(entity.attributes.get("forecast")) == 8 + + assert ( + entity.attributes.get("forecast")[5]["datetime"].strftime(DATETIME_FORMAT) + == "2020-04-28 12:00:00+0000" + ) + assert entity.attributes.get("forecast")[5]["condition"] == "cloudy" + assert entity.attributes.get("forecast")[5]["temperature"] == 11 + assert entity.attributes.get("forecast")[5]["wind_speed"] == 7 + assert entity.attributes.get("forecast")[5]["wind_bearing"] == "ESE" diff --git a/tests/fixtures/metoffice.json b/tests/fixtures/metoffice.json index c2b8707ca7a..22a0673c4dd 100644 --- a/tests/fixtures/metoffice.json +++ b/tests/fixtures/metoffice.json @@ -218,6 +218,7 @@ "U": "0", "$": "180" }, + { "D": "NW", "F": "10", @@ -1495,5 +1496,258 @@ } } } + }, + "kingslynn_daily": { + "SiteRep": { + "Wx": { + "Param": [ + { + "name": "FDm", + "units": "C", + "$": "Feels Like Day Maximum Temperature" + }, + { + "name": "FNm", + "units": "C", + "$": "Feels Like Night Minimum Temperature" + }, + { + "name": "Dm", + "units": "C", + "$": "Day Maximum Temperature" + }, + { + "name": "Nm", + "units": "C", + "$": "Night Minimum Temperature" + }, + { + "name": "Gn", + "units": "mph", + "$": "Wind Gust Noon" + }, + { + "name": "Gm", + "units": "mph", + "$": "Wind Gust Midnight" + }, + { + "name": "Hn", + "units": "%", + "$": "Screen Relative Humidity Noon" + }, + { + "name": "Hm", + "units": "%", + "$": "Screen Relative Humidity Midnight" + }, + { + "name": "V", + "units": "", + "$": "Visibility" + }, + { + "name": "D", + "units": "compass", + "$": "Wind Direction" + }, + { + "name": "S", + "units": "mph", + "$": "Wind Speed" + }, + { + "name": "U", + "units": "", + "$": "Max UV Index" + }, + { + "name": "W", + "units": "", + "$": "Weather Type" + }, + { + "name": "PPd", + "units": "%", + "$": "Precipitation Probability Day" + }, + { + "name": "PPn", + "units": "%", + "$": "Precipitation Probability Night" + } + ] + }, + "DV": { + "dataDate": "2020-04-25T08:00:00Z", + "type": "Forecast", + "Location": { + "i": "322380", + "lat": "52.7561", + "lon": "0.4019", + "name": "KING'S LYNN", + "country": "ENGLAND", + "continent": "EUROPE", + "elevation": "5.0", + "Period": [ + { + "type": "Day", + "value": "2020-04-25Z", + "Rep": [ + { + "D": "ESE", + "Gn": "4", + "Hn": "75", + "PPd": "9", + "S": "4", + "V": "VG", + "Dm": "9", + "FDm": "8", + "W": "8", + "U": "3", + "$": "Day" + }, + { + "D": "SSE", + "Gm": "16", + "Hm": "84", + "PPn": "0", + "S": "7", + "V": "VG", + "Nm": "7", + "FNm": "5", + "W": "0", + "$": "Night" + } + ] + }, + { + "type": "Day", + "value": "2020-04-26Z", + "Rep": [ + { + "D": "SSW", + "Gn": "13", + "Hn": "69", + "PPd": "0", + "S": "9", + "V": "VG", + "Dm": "13", + "FDm": "11", + "W": "1", + "U": "4", + "$": "Day" + }, + { + "D": "SSW", + "Gm": "13", + "Hm": "75", + "PPn": "5", + "S": "7", + "V": "GO", + "Nm": "11", + "FNm": "10", + "W": "7", + "$": "Night" + } + ] + }, + { + "type": "Day", + "value": "2020-04-27Z", + "Rep": [ + { + "D": "NW", + "Gn": "11", + "Hn": "78", + "PPd": "36", + "S": "4", + "V": "VG", + "Dm": "10", + "FDm": "9", + "W": "7", + "U": "3", + "$": "Day" + }, + { + "D": "SE", + "Gm": "13", + "Hm": "85", + "PPn": "9", + "S": "7", + "V": "VG", + "Nm": "9", + "FNm": "7", + "W": "7", + "$": "Night" + } + ] + }, + { + "type": "Day", + "value": "2020-04-28Z", + "Rep": [ + { + "D": "ESE", + "Gn": "13", + "Hn": "77", + "PPd": "14", + "S": "7", + "V": "GO", + "Dm": "11", + "FDm": "9", + "W": "7", + "U": "3", + "$": "Day" + }, + { + "D": "SSE", + "Gm": "13", + "Hm": "87", + "PPn": "11", + "S": "7", + "V": "GO", + "Nm": "9", + "FNm": "7", + "W": "7", + "$": "Night" + } + ] + }, + { + "type": "Day", + "value": "2020-04-29Z", + "Rep": [ + { + "D": "SSE", + "Gn": "20", + "Hn": "75", + "PPd": "8", + "S": "11", + "V": "VG", + "Dm": "12", + "FDm": "10", + "W": "7", + "U": "3", + "$": "Day" + }, + { + "D": "SSE", + "Gm": "20", + "Hm": "86", + "PPn": "20", + "S": "11", + "V": "VG", + "Nm": "9", + "FNm": "7", + "W": "7", + "$": "Night" + } + ] + } + ] + } + } + } } } \ No newline at end of file From e56069558a9b5da30a1a97dbb667b2bbb444c5aa Mon Sep 17 00:00:00 2001 From: hesselonline Date: Sun, 27 Jun 2021 21:06:25 +0200 Subject: [PATCH 569/750] Refactor wallbox tests (#51094) * Changed Testing, removed custom exception Removed custom exceptions, reverted to builtin. Changed testing approach in all tests, now using the core interface to setup device and mock_requests to create test responses for all calls. * Reintroduce InvalidAuth exception in __init__ Remove reference to internal HA exception, Reintroduce custom exception * Removed duplicate entry in test_config_flow * removed tests from test_init that calling methods directly * Update tests/components/wallbox/__init__.py Removed duplicate add_to_hass call Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/wallbox/__init__.py | 14 -- .../components/wallbox/config_flow.py | 4 +- tests/components/wallbox/__init__.py | 43 ++++++ tests/components/wallbox/test_config_flow.py | 141 ++++++++++-------- tests/components/wallbox/test_init.py | 136 +---------------- tests/components/wallbox/test_sensor.py | 69 ++------- 6 files changed, 137 insertions(+), 270 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 63fe37732dc..aeacee9b943 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -121,19 +121,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - def __init__(self, msg=""): - """Create a log record.""" - super().__init__() - _LOGGER.error("Cannot connect to Wallbox API. %s", msg) - - class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" - - def __init__(self, msg=""): - """Create a log record.""" - super().__init__() - _LOGGER.error("Cannot authenticate with Wallbox API. %s", msg) diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index 69b01d96c40..f9fdef3c5af 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from . import CannotConnect, InvalidAuth, WallboxHub +from . import InvalidAuth, WallboxHub from .const import CONF_STATION, DOMAIN COMPONENT_DOMAIN = DOMAIN @@ -46,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): try: info = await validate_input(self.hass, user_input) - except CannotConnect: + except ConnectionError: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 35bf3cee242..21554cc4456 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -1 +1,44 @@ """Tests for the Wallbox integration.""" + +import json + +import requests_mock + +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "44.697"}' +) + + +async def setup_integration(hass): + """Test wallbox sensor class setup.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", + ) + + entry.add_to_hass(hass) + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=test_response, + status_code=200, + ) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 074f67abe2c..6b5a05a3486 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -1,13 +1,18 @@ """Test the Wallbox config flow.""" +import json from unittest.mock import patch -from voluptuous.schema_builder import raises +import requests_mock from homeassistant import config_entries, data_entry_flow -from homeassistant.components.wallbox import CannotConnect, InvalidAuth, config_flow +from homeassistant.components.wallbox import InvalidAuth, config_flow from homeassistant.components.wallbox.const import DOMAIN from homeassistant.core import HomeAssistant +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -42,16 +47,31 @@ async def test_form_invalid_auth(hass): assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): +async def test_form_cannot_authenticate(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", - side_effect=CannotConnect, - ): + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=403, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=403, + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -65,64 +85,61 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "invalid_auth"} -async def test_validate_input(hass): - """Test we can validate input.""" - data = { - "station": "12345", - "username": "test-username", - "password": "test-password", - } +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - def alternate_authenticate_method(): - return None + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=404, + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) - def alternate_get_charger_status_method(station): - data = '{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}' - return data - - with patch( - "wallbox.Wallbox.authenticate", - side_effect=alternate_authenticate_method, - ), patch( - "wallbox.Wallbox.getChargerStatus", - side_effect=alternate_get_charger_status_method, - ): - - result = await config_flow.validate_input(hass, data) - - assert result == {"title": "Wallbox Portal"} + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} -async def test_configflow_class(): - """Test configFlow class.""" - configflow = config_flow.ConfigFlow() - assert configflow +async def test_form_validate_input(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - with patch( - "homeassistant.components.wallbox.config_flow.validate_input", - side_effect=TypeError, - ), raises(Exception): - assert await configflow.async_step_user(True) + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=200, + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) - with patch( - "homeassistant.components.wallbox.config_flow.validate_input", - side_effect=CannotConnect, - ), raises(Exception): - assert await configflow.async_step_user(True) - - with patch( - "homeassistant.components.wallbox.config_flow.validate_input", - ), raises(Exception): - assert await configflow.async_step_user(True) - - -def test_cannot_connect_class(): - """Test cannot Connect class.""" - cannot_connect = CannotConnect - assert cannot_connect - - -def test_invalid_auth_class(): - """Test invalid auth class.""" - invalid_auth = InvalidAuth - assert invalid_auth + assert result2["title"] == "Wallbox Portal" + assert result2["data"]["station"] == "12345" diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index d03974d1dfe..874629bac3e 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -1,16 +1,12 @@ """Test Wallbox Init Component.""" import json -import pytest -import requests_mock -from voluptuous.schema_builder import raises - -from homeassistant.components import wallbox from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.components.wallbox import setup_integration entry = MockConfigEntry( domain=DOMAIN, @@ -31,135 +27,9 @@ test_response_rounding_error = json.loads( ) -async def test_wallbox_setup_entry(hass: HomeAssistant): - """Test Wallbox Setup.""" - with requests_mock.Mocker() as m: - m.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, - ) - m.get( - "https://api.wall-box.com/chargers/status/12345", - text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', - status_code=200, - ) - assert await wallbox.async_setup_entry(hass, entry) - - with requests_mock.Mocker() as m, raises(ConnectionError): - m.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":404}', - status_code=404, - ) - assert await wallbox.async_setup_entry(hass, entry) is False - - async def test_wallbox_unload_entry(hass: HomeAssistant): """Test Wallbox Unload.""" - hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} - assert await wallbox.async_unload_entry(hass, entry) + await setup_integration(hass) - hass.data[DOMAIN] = {"fail_entry": entry} - - with pytest.raises(KeyError): - await wallbox.async_unload_entry(hass, entry) - - -async def test_get_data(hass: HomeAssistant): - """Test hub class, get_data.""" - - station = ("12345",) - username = ("test-username",) - password = "test-password" - - hub = wallbox.WallboxHub(station, username, password, hass) - - with requests_mock.Mocker() as m: - m.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, - ) - m.get( - "https://api.wall-box.com/chargers/status/('12345',)", - json=test_response, - status_code=200, - ) - assert await hub.async_get_data() - - -async def test_get_data_rounding_error(hass: HomeAssistant): - """Test hub class, get_data with rounding error.""" - - station = ("12345",) - username = ("test-username",) - password = "test-password" - - hub = wallbox.WallboxHub(station, username, password, hass) - - with requests_mock.Mocker() as m: - m.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, - ) - m.get( - "https://api.wall-box.com/chargers/status/('12345',)", - json=test_response_rounding_error, - status_code=200, - ) - assert await hub.async_get_data() - - -async def test_authentication_exception(hass: HomeAssistant): - """Test hub class, authentication raises exception.""" - - station = ("12345",) - username = ("test-username",) - password = "test-password" - - hub = wallbox.WallboxHub(station, username, password, hass) - - with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): - m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) - - assert await hub.async_authenticate() - - with requests_mock.Mocker() as m, raises(ConnectionError): - m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) - - assert await hub.async_authenticate() - - with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): - m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) - m.get( - "https://api.wall-box.com/chargers/status/test", - json=test_response, - status_code=403, - ) - assert await hub.async_get_data() - - -async def test_get_data_exception(hass: HomeAssistant): - """Test hub class, authentication raises exception.""" - - station = ("12345",) - username = ("test-username",) - password = "test-password" - - hub = wallbox.WallboxHub(station, username, password, hass) - - with requests_mock.Mocker() as m, raises(ConnectionError): - m.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, - ) - m.get( - "https://api.wall-box.com/chargers/status/('12345',)", - text="data", - status_code=404, - ) - assert await hub.async_get_data() + assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 5c0c3511a30..b88ed094fda 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,13 +1,10 @@ """Test Wallbox Switch component.""" -import json -from unittest.mock import MagicMock - -from homeassistant.components.wallbox import sensor from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry +from tests.components.wallbox import setup_integration entry = MockConfigEntry( domain=DOMAIN, @@ -19,63 +16,17 @@ entry = MockConfigEntry( entry_id="testEntry", ) -test_response = json.loads( - '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' -) -test_response_rounding_error = json.loads( - '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' -) - -CONF_STATION = ("12345",) -CONF_USERNAME = ("test-username",) -CONF_PASSWORD = "test-password" - -# wallbox = WallboxHub(CONF_STATION, CONF_USERNAME, CONF_PASSWORD, hass) - - -async def test_wallbox_sensor_class(): +async def test_wallbox_sensor_class(hass): """Test wallbox sensor class.""" - coordinator = MagicMock(return_value="connected") - idx = 1 - ent = "charging_power" + await setup_integration(hass) - wallboxSensor = sensor.WallboxSensor(coordinator, idx, ent, entry) + state = hass.states.get("sensor.mock_title_charging_power") + assert state.attributes["unit_of_measurement"] == "kW" + assert state.attributes["icon"] == "mdi:ev-station" + assert state.name == "Mock Title Charging Power" - assert wallboxSensor.icon == "mdi:ev-station" - assert wallboxSensor.unit_of_measurement == "kW" - assert wallboxSensor.name == "Mock Title Charging Power" - assert wallboxSensor.state - - -# async def test_wallbox_updater(hass: HomeAssistantType): -# """Test wallbox updater.""" -# with requests_mock.Mocker() as m: -# m.get( -# "https://api.wall-box.com/auth/token/user", -# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', -# status_code=200, -# ) -# m.get( -# "https://api.wall-box.com/chargers/status/('12345',)", -# json=test_response, -# status_code=200, -# ) -# await sensor.wallbox_updater(wallbox, hass) - - -# async def test_wallbox_updater_rounding_error(hass: HomeAssistantType): -# """Test wallbox updater rounding error.""" -# with requests_mock.Mocker() as m: -# m.get( -# "https://api.wall-box.com/auth/token/user", -# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', -# status_code=200, -# ) -# m.get( -# "https://api.wall-box.com/chargers/status/('12345',)", -# json=test_response_rounding_error, -# status_code=200, -# ) -# await sensor.wallbox_updater(wallbox, hass) + state = hass.states.get("sensor.mock_title_charging_speed") + assert state.attributes["icon"] == "mdi:speedometer" + assert state.name == "Mock Title Charging Speed" From e6e39a67f4d1a4f9dd2234ba01f4acee07d43170 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 27 Jun 2021 21:09:03 +0200 Subject: [PATCH 570/750] AsusWRT code improvements for sensors and related tests (#51822) * Sensors implementation and tests improvements * Remove check for unexpected condition --- homeassistant/components/asuswrt/const.py | 8 +-- homeassistant/components/asuswrt/router.py | 73 ++++++++++------------ homeassistant/components/asuswrt/sensor.py | 42 +++++-------- tests/components/asuswrt/test_sensor.py | 56 ++++++----------- 4 files changed, 70 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index a8977a77ea8..b450030ea3a 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -21,8 +21,6 @@ PROTOCOL_SSH = "ssh" PROTOCOL_TELNET = "telnet" # Sensors -SENSOR_CONNECTED_DEVICE = "sensor_connected_device" -SENSOR_RX_BYTES = "sensor_rx_bytes" -SENSOR_TX_BYTES = "sensor_tx_bytes" -SENSOR_RX_RATES = "sensor_rx_rates" -SENSOR_TX_RATES = "sensor_tx_rates" +SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"] +SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"] +SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 4cea9148470..b94de51b2fb 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -40,11 +40,9 @@ from .const import ( DEFAULT_TRACK_UNKNOWN, DOMAIN, PROTOCOL_TELNET, - SENSOR_CONNECTED_DEVICE, - SENSOR_RX_BYTES, - SENSOR_RX_RATES, - SENSOR_TX_BYTES, - SENSOR_TX_RATES, + SENSORS_BYTES, + SENSORS_CONNECTED_DEVICE, + SENSORS_RATES, ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] @@ -61,6 +59,16 @@ SENSORS_TYPE_RATES = "sensors_rates" _LOGGER = logging.getLogger(__name__) +def _get_dict(keys: list, values: list) -> dict[str, Any]: + """Create a dict from a list of keys and values.""" + ret_dict: dict[str, Any] = dict.fromkeys(keys) + + for index, key in enumerate(ret_dict): + ret_dict[key] = values[index] + + return ret_dict + + class AsusWrtSensorDataHandler: """Data handler for AsusWrt sensor.""" @@ -72,33 +80,25 @@ class AsusWrtSensorDataHandler: async def _get_connected_devices(self): """Return number of connected devices.""" - return {SENSOR_CONNECTED_DEVICE: self._connected_devices} + return {SENSORS_CONNECTED_DEVICE[0]: self._connected_devices} async def _get_bytes(self): """Fetch byte information from the router.""" - ret_dict: dict[str, Any] = {} try: datas = await self._api.async_get_bytes_total() - except OSError as exc: - raise UpdateFailed from exc + except (OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc - ret_dict[SENSOR_RX_BYTES] = datas[0] - ret_dict[SENSOR_TX_BYTES] = datas[1] - - return ret_dict + return _get_dict(SENSORS_BYTES, datas) async def _get_rates(self): """Fetch rates information from the router.""" - ret_dict: dict[str, Any] = {} try: rates = await self._api.async_get_current_transfer_rates() - except OSError as exc: - raise UpdateFailed from exc + except (OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc - ret_dict[SENSOR_RX_RATES] = rates[0] - ret_dict[SENSOR_TX_RATES] = rates[1] - - return ret_dict + return _get_dict(SENSORS_RATES, rates) def update_device_count(self, conn_devices: int): """Update connected devices attribute.""" @@ -315,29 +315,20 @@ class AsusWrtRouter: self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) self._sensors_data_handler.update_device_count(self._connected_devices) - conn_dev_coordinator = await self._sensors_data_handler.get_coordinator( - SENSORS_TYPE_COUNT, False - ) - self._sensors_coordinator[SENSORS_TYPE_COUNT] = { - KEY_COORDINATOR: conn_dev_coordinator, - KEY_SENSORS: [SENSOR_CONNECTED_DEVICE], + sensors_types = { + SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, + SENSORS_TYPE_BYTES: SENSORS_BYTES, + SENSORS_TYPE_RATES: SENSORS_RATES, } - bytes_coordinator = await self._sensors_data_handler.get_coordinator( - SENSORS_TYPE_BYTES - ) - self._sensors_coordinator[SENSORS_TYPE_BYTES] = { - KEY_COORDINATOR: bytes_coordinator, - KEY_SENSORS: [SENSOR_RX_BYTES, SENSOR_TX_BYTES], - } - - rates_coordinator = await self._sensors_data_handler.get_coordinator( - SENSORS_TYPE_RATES - ) - self._sensors_coordinator[SENSORS_TYPE_RATES] = { - KEY_COORDINATOR: rates_coordinator, - KEY_SENSORS: [SENSOR_RX_RATES, SENSOR_TX_RATES], - } + for sensor_type, sensor_names in sensors_types.items(): + coordinator = await self._sensors_data_handler.get_coordinator( + sensor_type, sensor_type != SENSORS_TYPE_COUNT + ) + self._sensors_coordinator[sensor_type] = { + KEY_COORDINATOR: coordinator, + KEY_SENSORS: sensor_names, + } async def _update_unpolled_sensors(self) -> None: """Request refresh for AsusWrt unpolled sensors.""" diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 6ec077620f6..cfa8748d2ba 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -18,11 +18,9 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( DATA_ASUSWRT, DOMAIN, - SENSOR_CONNECTED_DEVICE, - SENSOR_RX_BYTES, - SENSOR_RX_RATES, - SENSOR_TX_BYTES, - SENSOR_TX_RATES, + SENSORS_BYTES, + SENSORS_CONNECTED_DEVICE, + SENSORS_RATES, ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter @@ -38,41 +36,36 @@ SENSOR_DEFAULT_ENABLED = "default_enabled" UNIT_DEVICES = "Devices" CONNECTION_SENSORS = { - SENSOR_CONNECTED_DEVICE: { + SENSORS_CONNECTED_DEVICE[0]: { SENSOR_NAME: "Devices Connected", SENSOR_UNIT: UNIT_DEVICES, SENSOR_FACTOR: 0, SENSOR_ICON: "mdi:router-network", - SENSOR_DEVICE_CLASS: None, SENSOR_DEFAULT_ENABLED: True, }, - SENSOR_RX_RATES: { + SENSORS_RATES[0]: { SENSOR_NAME: "Download Speed", SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, SENSOR_FACTOR: 125000, SENSOR_ICON: "mdi:download-network", - SENSOR_DEVICE_CLASS: None, }, - SENSOR_TX_RATES: { + SENSORS_RATES[1]: { SENSOR_NAME: "Upload Speed", SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, SENSOR_FACTOR: 125000, SENSOR_ICON: "mdi:upload-network", - SENSOR_DEVICE_CLASS: None, }, - SENSOR_RX_BYTES: { + SENSORS_BYTES[0]: { SENSOR_NAME: "Download", SENSOR_UNIT: DATA_GIGABYTES, SENSOR_FACTOR: 1000000000, SENSOR_ICON: "mdi:download", - SENSOR_DEVICE_CLASS: None, }, - SENSOR_TX_BYTES: { + SENSORS_BYTES[1]: { SENSOR_NAME: "Upload", SENSOR_UNIT: DATA_GIGABYTES, SENSOR_FACTOR: 1000000000, SENSOR_ICON: "mdi:upload", - SENSOR_DEVICE_CLASS: None, }, } @@ -108,24 +101,21 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): coordinator: DataUpdateCoordinator, router: AsusWrtRouter, sensor_type: str, - sensor: dict[str, Any], + sensor_def: dict[str, Any], ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) self._router = router self._sensor_type = sensor_type - self._name = f"{DEFAULT_PREFIX} {sensor[SENSOR_NAME]}" + self._sensor_def = sensor_def + self._name = f"{DEFAULT_PREFIX} {sensor_def[SENSOR_NAME]}" self._unique_id = f"{DOMAIN} {self._name}" - self._unit = sensor[SENSOR_UNIT] - self._factor = sensor[SENSOR_FACTOR] - self._icon = sensor[SENSOR_ICON] - self._device_class = sensor[SENSOR_DEVICE_CLASS] - self._default_enabled = sensor.get(SENSOR_DEFAULT_ENABLED, False) + self._factor = sensor_def.get(SENSOR_FACTOR) @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._default_enabled + return self._sensor_def.get(SENSOR_DEFAULT_ENABLED, False) @property def state(self) -> str: @@ -150,17 +140,17 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): @property def unit_of_measurement(self) -> str: """Return the unit.""" - return self._unit + return self._sensor_def.get(SENSOR_UNIT) @property def icon(self) -> str: """Return the icon.""" - return self._icon + return self._sensor_def.get(SENSOR_ICON) @property def device_class(self) -> str: """Return the device_class.""" - return self._device_class + return self._sensor_def.get(SENSOR_DEVICE_CLASS) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 87c3fadb978..cbdded64ff9 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_NOT_HOME, ) from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed @@ -39,6 +40,14 @@ CONFIG_DATA = { MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] +SENSOR_NAMES = [ + "Devices Connected", + "Download Speed", + "Download", + "Upload Speed", + "Upload", +] + @pytest.fixture(name="mock_devices") def mock_devices_fixture(): @@ -88,46 +97,19 @@ async def test_sensors(hass, connect, mock_devices): # init variable unique_id = DOMAIN - name_prefix = DEFAULT_PREFIX - obj_prefix = name_prefix.lower() + obj_prefix = slugify(DEFAULT_PREFIX) sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}" # Pre-enable the status sensor - entity_reg.async_get_or_create( - sensor.DOMAIN, - DOMAIN, - f"{unique_id} {name_prefix} Devices Connected", - suggested_object_id=f"{obj_prefix}_devices_connected", - disabled_by=None, - ) - entity_reg.async_get_or_create( - sensor.DOMAIN, - DOMAIN, - f"{unique_id} {name_prefix} Download Speed", - suggested_object_id=f"{obj_prefix}_download_speed", - disabled_by=None, - ) - entity_reg.async_get_or_create( - sensor.DOMAIN, - DOMAIN, - f"{unique_id} {name_prefix} Download", - suggested_object_id=f"{obj_prefix}_download", - disabled_by=None, - ) - entity_reg.async_get_or_create( - sensor.DOMAIN, - DOMAIN, - f"{unique_id} {name_prefix} Upload Speed", - suggested_object_id=f"{obj_prefix}_upload_speed", - disabled_by=None, - ) - entity_reg.async_get_or_create( - sensor.DOMAIN, - DOMAIN, - f"{unique_id} {name_prefix} Upload", - suggested_object_id=f"{obj_prefix}_upload", - disabled_by=None, - ) + for sensor_name in SENSOR_NAMES: + sensor_id = slugify(sensor_name) + entity_reg.async_get_or_create( + sensor.DOMAIN, + DOMAIN, + f"{unique_id} {DEFAULT_PREFIX} {sensor_name}", + suggested_object_id=f"{obj_prefix}_{sensor_id}", + disabled_by=None, + ) config_entry.add_to_hass(hass) From bd399d17a78e9fb8029737135a52cc3091ac6c97 Mon Sep 17 00:00:00 2001 From: SgtBatten Date: Mon, 28 Jun 2021 05:13:26 +1000 Subject: [PATCH 571/750] Add support for 4th fan speed in izone A/C systems (#51969) * Add TOP fan speed My a/c is 4 speed and the top speed is reported as top or boost. i.e it supports: low med high boost auto * add support for top fan speed Aircons with 4 fan speeds. i.e low, med, high, top/boost * Update manifest.json Bump version to 1.1.5 * Update climate.py * Bump Izone to 1.1.5 * Update climate.py * fix isort failure * Use v1.1.6 Co-authored-by: Penny Wood Co-authored-by: Swamp-Ig --- homeassistant/components/climate/const.py | 1 + homeassistant/components/izone/climate.py | 2 ++ homeassistant/components/izone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index af6c9364b18..55387d71438 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -63,6 +63,7 @@ FAN_AUTO = "auto" FAN_LOW = "low" FAN_MEDIUM = "medium" FAN_HIGH = "high" +FAN_TOP = "top" FAN_MIDDLE = "middle" FAN_FOCUS = "focus" FAN_DIFFUSE = "diffuse" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 253bdc6cb2b..968f13748b2 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -12,6 +12,7 @@ from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, + FAN_TOP, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -54,6 +55,7 @@ _IZONE_FAN_TO_HA = { Controller.Fan.LOW: FAN_LOW, Controller.Fan.MED: FAN_MEDIUM, Controller.Fan.HIGH: FAN_HIGH, + Controller.Fan.TOP: FAN_TOP, Controller.Fan.AUTO: FAN_AUTO, } diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 0a2b8f82fe5..6da770f5c0b 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,7 +2,7 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.4"], + "requirements": ["python-izone==1.1.6"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index c261d2b478d..92e2151b737 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1833,7 +1833,7 @@ python-gitlab==1.6.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.1.4 +python-izone==1.1.6 # homeassistant.components.joaoapps_join python-join-api==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc9b9ac9736..5128a8ab41d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1015,7 +1015,7 @@ python-ecobee-api==0.2.11 python-forecastio==1.4.0 # homeassistant.components.izone -python-izone==1.1.4 +python-izone==1.1.6 # homeassistant.components.juicenet python-juicenet==1.0.2 From c404a196c2bb3b12e1056fa6dd8f4d07473bfe31 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 27 Jun 2021 15:21:15 -0400 Subject: [PATCH 572/750] Allow creating ZHA groups with specific IDs (#50781) --- homeassistant/components/zha/api.py | 4 +++- homeassistant/components/zha/core/gateway.py | 10 ++++++---- tests/components/zha/test_gateway.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 053162010e8..403af7c6612 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -363,6 +363,7 @@ def cv_group_member(value: Any) -> GroupMember: { vol.Required(TYPE): "zha/group/add", vol.Required(GROUP_NAME): cv.string, + vol.Optional(GROUP_ID): cv.positive_int, vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) @@ -371,7 +372,8 @@ async def websocket_add_group(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_name = msg[GROUP_NAME] members = msg.get(ATTR_MEMBERS) - group = await zha_gateway.async_create_zigpy_group(group_name, members) + group_id = msg.get(GROUP_ID) + group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id) connection.send_result(msg[ID], group.group_info) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3ba5627cde8..491f1a29774 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -616,13 +616,15 @@ class ZHAGateway: zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: list[GroupMember] + self, name: str, members: list[GroupMember], group_id: int = None ) -> ZhaGroupType: """Create a new Zigpy Zigbee group.""" # we start with two to fill any gaps from a user removing existing groups - group_id = 2 - while group_id in self.groups: - group_id += 1 + + if group_id is None: + group_id = 2 + while group_id in self.groups: + group_id += 1 # guard against group already existing if self.async_get_group_by_name(name) is None: diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 5d7f66bacd5..4b3a9bec50c 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -176,6 +176,24 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord assert member.device.ieee in [device_light_1.ieee] +async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): + """Test creating a group with a specific ID.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", [GroupMember(device_light_1.ieee, 1)], group_id=0x1234 + ) + await hass.async_block_till_done() + + assert len(zha_group.members) == 1 + assert zha_group.members[0].device is device_light_1 + assert zha_group.group_id == 0x1234 + + async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): """Test saving data after a delay.""" zha_gateway = get_zha_gateway(hass) From 0d689eefd6a21873baaa526b99989badd5495240 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Jun 2021 22:57:08 +0200 Subject: [PATCH 573/750] Make Philips TV notify service optional (#50691) * Make event service optional * Correct strings for rename * Drop unload we are just testing config flow here --- .../components/philips_js/__init__.py | 16 ++++++-- .../components/philips_js/config_flow.py | 31 ++++++++++++++- homeassistant/components/philips_js/const.py | 1 + .../components/philips_js/strings.json | 11 +++++- .../philips_js/translations/en.json | 9 +++++ .../components/philips_js/test_config_flow.py | 39 ++++++++++++++++--- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 587a5f8c4f2..1006df699f4 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, c from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CONF_ALLOW_NOTIFY, DOMAIN PLATFORMS = ["media_player", "light", "remote"] @@ -36,8 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), ) - - coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) + coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options) await coordinator.async_refresh() hass.data.setdefault(DOMAIN, {}) @@ -45,9 +44,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_entry)) + return True +async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -94,9 +100,10 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" - def __init__(self, hass, api: PhilipsTV) -> None: + def __init__(self, hass, api: PhilipsTV, options: dict) -> None: """Set up the coordinator.""" self.api = api + self.options = options self._notify_future: asyncio.Task | None = None @callback @@ -127,6 +134,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self.api.on and self.api.powerstate == "On" and self.api.notify_change_supported + and self.options.get(CONF_ALLOW_NOTIFY, False) ) async def _notify_task(self): diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 84303e6ca92..59403b2ec86 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from . import LOGGER -from .const import CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN +from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN async def validate_input( @@ -154,3 +154,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + @staticmethod + @core.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 AEMET.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_ALLOW_NOTIFY, + default=self.config_entry.options.get(CONF_ALLOW_NOTIFY), + ): bool, + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/philips_js/const.py b/homeassistant/components/philips_js/const.py index 5769a8979ce..5d1141a8fb9 100644 --- a/homeassistant/components/philips_js/const.py +++ b/homeassistant/components/philips_js/const.py @@ -2,6 +2,7 @@ DOMAIN = "philips_js" CONF_SYSTEM = "system" +CONF_ALLOW_NOTIFY = "allow_notify" CONST_APP_ID = "homeassistant.io" CONST_APP_NAME = "Home Assistant" diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index 5c8f08eff6a..3e6d4f494d3 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -20,11 +20,20 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "pairing_failure": "Unable to pair: {error_id}", "invalid_pin": "Invalid PIN" -}, + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Allow usage of data notification service." + } + } + } + }, "device_automation": { "trigger_type": { "turn_on": "Device is requested to turn on" diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index ea254a3873d..2cc009d4f39 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -25,6 +25,15 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Allow usage of data notification service." + } + } + } + }, "device_automation": { "trigger_type": { "turn_on": "Device is requested to turn on" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 4841cd5a940..f3ab44844a2 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import ANY, patch from haphilipsjs import PairingFailure from pytest import fixture -from homeassistant import config_entries -from homeassistant.components.philips_js.const import DOMAIN +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN from . import ( MOCK_CONFIG, @@ -17,13 +17,17 @@ from . import ( MOCK_USERNAME, ) +from tests.common import MockConfigEntry -@fixture(autouse=True) -def mock_setup_entry(): + +@fixture(autouse=True, name="mock_setup_entry") +def mock_setup_entry_fixture(): """Disable component setup.""" with patch( "homeassistant.components.philips_js.async_setup_entry", return_value=True - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.philips_js.async_unload_entry", return_value=True + ): yield mock_setup_entry @@ -226,3 +230,28 @@ async def test_pair_grant_failed(hass, mock_tv_pairable, mock_setup_entry): "reason": "pairing_failure", "type": "abort", } + + +async def test_options_flow(hass): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="123456", + data=MOCK_CONFIG_PAIRED, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + 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"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_ALLOW_NOTIFY: True} From 8b47faa840c0acbb8039bfb5851b3133260c6865 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 27 Jun 2021 15:58:49 -0500 Subject: [PATCH 574/750] Remove undo listener variable in cloudflare (#52227) * remove undo listener variable in cloudflare * Update const.py * Update __init__.py * Update __init__.py --- homeassistant/components/cloudflare/__init__.py | 17 +++++------------ homeassistant/components/cloudflare/const.py | 3 --- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index e461e34c9a2..ed4c54966cf 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -19,13 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from .const import ( - CONF_RECORDS, - DATA_UNDO_UPDATE_INTERVAL, - DEFAULT_UPDATE_INTERVAL, - DOMAIN, - SERVICE_UPDATE_RECORDS, -) +from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE_RECORDS _LOGGER = logging.getLogger(__name__) @@ -64,12 +58,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) update_interval = timedelta(minutes=DEFAULT_UPDATE_INTERVAL) - undo_interval = async_track_time_interval(hass, update_records, update_interval) + entry.async_on_unload( + async_track_time_interval(hass, update_records, update_interval) + ) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_UNDO_UPDATE_INTERVAL: undo_interval, - } + hass.data[DOMAIN][entry.entry_id] = {} hass.services.async_register(DOMAIN, SERVICE_UPDATE_RECORDS, update_records_service) @@ -78,7 +72,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Cloudflare config entry.""" - hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_INTERVAL]() hass.data[DOMAIN].pop(entry.entry_id) return True diff --git a/homeassistant/components/cloudflare/const.py b/homeassistant/components/cloudflare/const.py index 0bdce7b9a92..4952b3768b0 100644 --- a/homeassistant/components/cloudflare/const.py +++ b/homeassistant/components/cloudflare/const.py @@ -5,9 +5,6 @@ DOMAIN = "cloudflare" # Config CONF_RECORDS = "records" -# Data -DATA_UNDO_UPDATE_INTERVAL = "undo_update_interval" - # Defaults DEFAULT_UPDATE_INTERVAL = 60 # in minutes From 5c5e43afc127fe420f314c01ee2e78d347631adc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 28 Jun 2021 00:10:15 +0000 Subject: [PATCH 575/750] [ci skip] Translation update --- .../demo/translations/select.hu.json | 9 ++++++ .../components/dsmr/translations/hu.json | 21 ++++++++++++- .../forecast_solar/translations/et.json | 31 +++++++++++++++++++ .../forecast_solar/translations/hu.json | 11 +++++++ .../forecast_solar/translations/ru.json | 31 +++++++++++++++++++ .../forecast_solar/translations/zh-Hant.json | 31 +++++++++++++++++++ .../philips_js/translations/en.json | 10 +++--- .../pvpc_hourly_pricing/translations/hu.json | 7 +++++ .../components/select/translations/hu.json | 14 +++++++++ .../xiaomi_miio/translations/hu.json | 12 +++++++ .../components/zwave_js/translations/hu.json | 13 ++++++++ 11 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/demo/translations/select.hu.json create mode 100644 homeassistant/components/forecast_solar/translations/et.json create mode 100644 homeassistant/components/forecast_solar/translations/hu.json create mode 100644 homeassistant/components/forecast_solar/translations/ru.json create mode 100644 homeassistant/components/forecast_solar/translations/zh-Hant.json create mode 100644 homeassistant/components/select/translations/hu.json diff --git a/homeassistant/components/demo/translations/select.hu.json b/homeassistant/components/demo/translations/select.hu.json new file mode 100644 index 00000000000..4afeff7b1d3 --- /dev/null +++ b/homeassistant/components/demo/translations/select.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "F\u00e9nysebess\u00e9g", + "ludicrous_speed": "Hihetetlen sebess\u00e9g", + "ridiculous_speed": "K\u00e9ptelen sebess\u00e9g" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json index 930b739fb18..76ad4dc653f 100644 --- a/homeassistant/components/dsmr/translations/hu.json +++ b/homeassistant/components/dsmr/translations/hu.json @@ -1,7 +1,26 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_communicate": "Nem siker\u00fclt csatlakozni." + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa" + } + }, + "setup_serial": { + "data": { + "port": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" + }, + "title": "Eszk\u00f6z" + }, + "user": { + "data": { + "type": "Kapcsolat t\u00edpusa" + } + } } }, "options": { diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json new file mode 100644 index 00000000000..6dddf4a7496 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Asimuut (360 kraadi, 0 = p\u00f5hi, 90 = ida, 180 = l\u00f5una, 270 = l\u00e4\u00e4s)", + "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides", + "name": "Nimi" + }, + "description": "Sisesta oma p\u00e4ikesepaneelide andmed. Kui v\u00e4li on ebaselge, loe dokumentatsiooni." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API v\u00f5ti (valikuline)", + "azimuth": "Asimuut (360 kraadi, 0 = p\u00f5hi, 90 = ida, 180 = l\u00f5una, 270 = l\u00e4\u00e4s)", + "damping": "Summutustegur: reguleerib tulemusi hommikul ja \u00f5htul", + "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)", + "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides" + }, + "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui on ebaselge." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json new file mode 100644 index 00000000000..b863d10e907 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json new file mode 100644 index 00000000000..1becfb2f5cb --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432, 0 = \u0441\u0435\u0432\u0435\u0440, 90 = \u0432\u043e\u0441\u0442\u043e\u043a, 180 = \u044e\u0433, 270 = \u0437\u0430\u043f\u0430\u0434)", + "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u044f\u0445." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API Forecast.Solar (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432, 0 = \u0441\u0435\u0432\u0435\u0440, 90 = \u0432\u043e\u0441\u0442\u043e\u043a, 180 = \u044e\u0433, 270 = \u0437\u0430\u043f\u0430\u0434)", + "damping": "\u0424\u0430\u043a\u0442\u043e\u0440 \u0437\u0430\u0442\u0443\u0445\u0430\u043d\u0438\u044f: \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0443\u0442\u0440\u043e\u043c \u0438 \u0432\u0435\u0447\u0435\u0440\u043e\u043c", + "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)", + "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)" + }, + "description": "\u042d\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 Forecast.Solar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json new file mode 100644 index 00000000000..43c7da0f593 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09", + "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "modules power": "\u592a\u967d\u80fd\u6a21\u7d44\u7e3d\u5cf0\u503c\u529f\u7387", + "name": "\u540d\u7a31" + }, + "description": "\u586b\u5beb\u592a\u967d\u80fd\u677f\u8cc7\u6599\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\uff0c\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API \u5bc6\u9470\uff08\u9078\u9805\uff09", + "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09", + "damping": "\u963b\u5c3c\u56e0\u7d20\uff1a\u8abf\u6574\u6e05\u6668\u8207\u508d\u665a\u7d50\u679c", + "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09", + "modules power": "\u7e3d\u5cf0\u503c\u529f\u7387" + }, + "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Solar.Forecast \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index 2cc009d4f39..1519bf440ee 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -25,6 +25,11 @@ } } }, + "device_automation": { + "trigger_type": { + "turn_on": "Device is requested to turn on" + } + }, "options": { "step": { "init": { @@ -33,10 +38,5 @@ } } } - }, - "device_automation": { - "trigger_type": { - "turn_on": "Device is requested to turn on" - } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index f5301e874ea..17bca647c18 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -3,5 +3,12 @@ "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" } + }, + "options": { + "step": { + "init": { + "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/select/translations/hu.json b/homeassistant/components/select/translations/hu.json new file mode 100644 index 00000000000..f93f0475c33 --- /dev/null +++ b/homeassistant/components/select/translations/hu.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "M\u00f3dos\u00edtsa a(z) {entity_name} be\u00e1ll\u00edt\u00e1st" + }, + "condition_type": { + "selected_option": "{entity_name} aktu\u00e1lisan kiv\u00e1lasztott opci\u00f3" + }, + "trigger_type": { + "current_option_changed": "{entity_name} opci\u00f3i megv\u00e1ltoztak" + } + }, + "title": "Kiv\u00e1laszt\u00e1s" +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 2dfe53db303..d8bf9dfd866 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -39,6 +39,11 @@ "reauth_confirm": { "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "select": { + "data": { + "select_device": "Miio eszk\u00f6z" + } + }, "user": { "data": { "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" @@ -47,5 +52,12 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "step": { + "init": { + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 87629666b09..484dbfa1824 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -40,5 +40,18 @@ } } }, + "options": { + "step": { + "configure_addon": { + "data": { + "log_level": "Napl\u00f3szint", + "network_key": "H\u00e1l\u00f3zati kulcs" + } + }, + "on_supervisor": { + "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + } + } + }, "title": "Z-Wave JS" } \ No newline at end of file From 5d3f3c756fc4ea4085252a1e23d0dd438c496699 Mon Sep 17 00:00:00 2001 From: Christopher Masto Date: Mon, 28 Jun 2021 03:19:49 -0400 Subject: [PATCH 576/750] Fix Fahrenheit to Celsius conversion in Prometheus exporter (#52212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit const.TEMP_FAHRENHEIT is "°F", but _unit_string converts this to "c", so the comparison never succeeds and we end up with temperatures in F but labeled C. --- homeassistant/components/prometheus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index b253daf559e..c74caa745f3 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -397,7 +397,7 @@ class PrometheusMetrics: try: value = self.state_as_number(state) - if unit == TEMP_FAHRENHEIT: + if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: value = fahrenheit_to_celsius(value) _metric.labels(**self._labels(state)).set(value) except ValueError: From bef8be92568a80f0f3d7b69a4e604585faef6488 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 28 Jun 2021 03:23:46 -0400 Subject: [PATCH 577/750] Support dynamic schema validation in device conditions and actions (#52007) * Allow integrations to provide dynamic schema validation in device conditions and actions * Add tests * re-add type * mypy --- homeassistant/helpers/condition.py | 2 ++ homeassistant/helpers/script.py | 5 ++++- tests/helpers/test_condition.py | 24 ++++++++++++++++++++++-- tests/helpers/test_script.py | 22 ++++++++++++++++++++-- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index cea79c4fc8f..6f5e7c40d22 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -972,6 +972,8 @@ async def async_validate_condition_config( platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "condition" ) + if hasattr(platform, "async_validate_condition_config"): + return await platform.async_validate_condition_config(hass, config) # type: ignore return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore return config diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ea3635888bb..156ceb8e612 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -256,7 +256,10 @@ async def async_validate_action_config( platform = await device_automation.async_get_device_automation_platform( hass, config[CONF_DOMAIN], "action" ) - config = platform.ACTION_SCHEMA(config) # type: ignore + if hasattr(platform, "async_validate_action_config"): + config = await platform.async_validate_action_config(hass, config) # type: ignore + else: + config = platform.ACTION_SCHEMA(config) # type: ignore elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION: if config[CONF_CONDITION] == "device": diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 38a9367e36d..b1cbff83e33 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,13 +1,20 @@ """Test the condition helper.""" from datetime import datetime -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from homeassistant.components import sun import homeassistant.components.automation as automation from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP -from homeassistant.const import ATTR_DEVICE_CLASS, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import condition, trace from homeassistant.helpers.template import Template @@ -2843,3 +2850,16 @@ async def test_trigger(hass): assert not test(hass, {"other_var": "123456"}) assert not test(hass, {"trigger": {"trigger_id": "123456"}}) assert test(hass, {"trigger": {"id": "123456"}}) + + +async def test_platform_async_validate_condition_config(hass): + """Test platform.async_validate_condition_config will be called if it exists.""" + config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"} + platform = AsyncMock() + with patch( + "homeassistant.helpers.condition.async_get_device_automation_platform", + return_value=platform, + ): + platform.async_validate_condition_config.return_value = config + await condition.async_validate_condition_config(hass, config) + platform.async_validate_condition_config.assert_awaited() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 0af8ff7d431..dfa5ce34ce7 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from types import MappingProxyType from unittest import mock -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from async_timeout import timeout import pytest @@ -15,7 +15,12 @@ import voluptuous as vol # Otherwise can't test just this file (import order issue) from homeassistant import exceptions import homeassistant.components.scene as scene -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + SERVICE_TURN_ON, +) from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback from homeassistant.exceptions import ConditionError, ServiceNotFound from homeassistant.helpers import config_validation as cv, script, trace @@ -3130,3 +3135,16 @@ async def test_breakpoints_2(hass): assert not script_obj.is_running assert script_obj.runs == 0 assert len(events) == 1 + + +async def test_platform_async_validate_action_config(hass): + """Test platform.async_validate_action_config will be called if it exists.""" + config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test"} + platform = AsyncMock() + with patch( + "homeassistant.helpers.script.device_automation.async_get_device_automation_platform", + return_value=platform, + ): + platform.async_validate_action_config.return_value = config + await script.async_validate_action_config(hass, config) + platform.async_validate_action_config.assert_awaited() From 9c84c2889f7a393b48905f99412c7def3f24efdb Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Mon, 28 Jun 2021 02:47:41 -0500 Subject: [PATCH 578/750] Modern forms switch platform (#52061) * Add switch platform to Modern Forms integration * Add reboot switch * Update lib to catch status from switches * lint ignore * Removed reboot switch * bump aiomodernforms for dependency cleanup --- .../components/modern_forms/__init__.py | 2 + .../components/modern_forms/manifest.json | 2 +- .../components/modern_forms/switch.py | 113 ++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/modern_forms/test_switch.py | 144 ++++++++++++++++++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/modern_forms/switch.py create mode 100644 tests/components/modern_forms/test_switch.py diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 7eee8ea9ad8..ca5e5388aaa 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -13,6 +13,7 @@ from aiomodernforms.models import Device as ModernFormsDeviceState from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST from homeassistant.core import HomeAssistant @@ -30,6 +31,7 @@ SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ LIGHT_DOMAIN, FAN_DOMAIN, + SWITCH_DOMAIN, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json index c2da0239fbe..1466537259b 100644 --- a/homeassistant/components/modern_forms/manifest.json +++ b/homeassistant/components/modern_forms/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/modern_forms", "requirements": [ - "aiomodernforms==0.1.5" + "aiomodernforms==0.1.8" ], "zeroconf": [ {"type":"_easylink._tcp.local.", "name":"wac*"} diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py new file mode 100644 index 00000000000..90d5d13d649 --- /dev/null +++ b/homeassistant/components/modern_forms/switch.py @@ -0,0 +1,113 @@ +"""Support for Modern Forms switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ( + ModernFormsDataUpdateCoordinator, + ModernFormsDeviceEntity, + modernforms_exception_handler, +) +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Modern Forms switch based on a config entry.""" + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + switches = [ + ModernFormsAwaySwitch(entry.entry_id, coordinator), + ModernFormsAdaptiveLearningSwitch(entry.entry_id, coordinator), + ] + async_add_entities(switches) + + +class ModernFormsSwitch(ModernFormsDeviceEntity, SwitchEntity): + """Defines a Modern Forms switch.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: ModernFormsDataUpdateCoordinator, + name: str, + icon: str, + key: str, + ) -> None: + """Initialize Modern Forms switch.""" + self._key = key + super().__init__( + entry_id=entry_id, coordinator=coordinator, name=name, icon=icon + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" + + +class ModernFormsAwaySwitch(ModernFormsSwitch): + """Defines a Modern Forms Away mode switch.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Away mode switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:airplane-takeoff", + key="away_mode", + name=f"{coordinator.data.info.device_name} Away Mode", + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.away_mode_enabled) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Modern Forms Away mode switch.""" + await self.coordinator.modern_forms.away(away=False) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Modern Forms Away mode switch.""" + await self.coordinator.modern_forms.away(away=True) + + +class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch): + """Defines a Modern Forms Adaptive Learning switch.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Adaptive Learning switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:school-outline", + key="adaptive_learning", + name=f"{coordinator.data.info.device_name} Adaptive Learning", + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.adaptive_learning_enabled) + + @modernforms_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the Modern Forms Adaptive Learning switch.""" + await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=False) + + @modernforms_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the Modern Forms Adaptive Learning switch.""" + await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=True) diff --git a/requirements_all.txt b/requirements_all.txt index 92e2151b737..746adfc31ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aiolip==1.1.4 aiolyric==1.0.7 # homeassistant.components.modern_forms -aiomodernforms==0.1.5 +aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast aiomusiccast==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5128a8ab41d..8661bd0f93e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiolip==1.1.4 aiolyric==1.0.7 # homeassistant.components.modern_forms -aiomodernforms==0.1.5 +aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast aiomusiccast==0.6 diff --git a/tests/components/modern_forms/test_switch.py b/tests/components/modern_forms/test_switch.py new file mode 100644 index 00000000000..963264e3ca1 --- /dev/null +++ b/tests/components/modern_forms/test_switch.py @@ -0,0 +1,144 @@ +"""Tests for the Modern Forms switch platform.""" +from unittest.mock import patch + +from aiomodernforms import ModernFormsConnectionError + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switch_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + entity_registry = er.async_get(hass) + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:airplane-takeoff" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.modernformsfan_away_mode") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF_away_mode" + + state = hass.states.get("switch.modernformsfan_adaptive_learning") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:school-outline" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.modernformsfan_adaptive_learning") + assert entry + assert entry.unique_id == "AA:BB:CC:DD:EE:FF_adaptive_learning" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + # Away Mode + with patch("aiomodernforms.ModernFormsDevice.away") as away_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + away_mock.assert_called_once_with(away=True) + + with patch("aiomodernforms.ModernFormsDevice.away") as away_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + away_mock.assert_called_once_with(away=False) + + # Adaptive Learning + with patch( + "aiomodernforms.ModernFormsDevice.adaptive_learning" + ) as adaptive_learning_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"}, + blocking=True, + ) + await hass.async_block_till_done() + adaptive_learning_mock.assert_called_once_with(adaptive_learning=True) + + with patch( + "aiomodernforms.ModernFormsDevice.adaptive_learning" + ) as adaptive_learning_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.modernformsfan_adaptive_learning"}, + blocking=True, + ) + await hass.async_block_till_done() + adaptive_learning_mock.assert_called_once_with(adaptive_learning=False) + + +async def test_switch_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + aioclient_mock.clear_requests() + aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state.state == STATE_OFF + assert "Invalid response from API" in caplog.text + + +async def test_switch_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the Modern Forms switches.""" + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( + "homeassistant.components.modern_forms.ModernFormsDevice.away", + side_effect=ModernFormsConnectionError, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.modernformsfan_away_mode") + assert state.state == STATE_UNAVAILABLE From 3d556f14a58b9cba8f40de4247473e24fa1eb180 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 28 Jun 2021 10:15:56 +0200 Subject: [PATCH 579/750] Remove `air_quality` platform from Airly integration (#52225) Co-authored-by: Martin Hjelmare --- homeassistant/components/airly/__init__.py | 13 +- homeassistant/components/airly/air_quality.py | 129 ------------------ homeassistant/components/airly/const.py | 41 ++++-- homeassistant/components/airly/model.py | 7 +- homeassistant/components/airly/sensor.py | 62 +++++++-- tests/components/airly/test_air_quality.py | 114 ---------------- tests/components/airly/test_init.py | 22 ++- tests/components/airly/test_sensor.py | 40 ++++++ 8 files changed, 158 insertions(+), 270 deletions(-) delete mode 100644 homeassistant/components/airly/air_quality.py delete mode 100644 tests/components/airly/test_air_quality.py diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 58899d76ef8..0304945e6d2 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -11,9 +11,11 @@ from airly import Airly from airly.exceptions import AirlyError import async_timeout +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -31,7 +33,7 @@ from .const import ( NO_AIRLY_SENSORS, ) -PLATFORMS = ["air_quality", "sensor"] +PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) @@ -111,6 +113,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Remove air_quality entities from registry if they exist + ent_reg = entity_registry.async_get(hass) + unique_id = f"{coordinator.latitude}-{coordinator.longitude}" + if entity_id := ent_reg.async_get_entity_id( + AIR_QUALITY_PLATFORM, DOMAIN, unique_id + ): + _LOGGER.debug("Removing deprecated air_quality entity %s", entity_id) + ent_reg.async_remove(entity_id) + return True diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py deleted file mode 100644 index 03c1084720d..00000000000 --- a/homeassistant/components/airly/air_quality.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Support for the Airly air_quality service.""" -from __future__ import annotations - -from typing import Any - -from homeassistant.components.air_quality import ( - ATTR_AQI, - ATTR_PM_2_5, - ATTR_PM_10, - AirQualityEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import AirlyDataUpdateCoordinator -from .const import ( - ATTR_API_ADVICE, - ATTR_API_CAQI, - ATTR_API_CAQI_DESCRIPTION, - ATTR_API_CAQI_LEVEL, - ATTR_API_PM10, - ATTR_API_PM10_LIMIT, - ATTR_API_PM10_PERCENT, - ATTR_API_PM25, - ATTR_API_PM25_LIMIT, - ATTR_API_PM25_PERCENT, - ATTRIBUTION, - DEFAULT_NAME, - DOMAIN, - LABEL_ADVICE, - MANUFACTURER, -) - -LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description" -LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" -LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" -LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" -LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" -LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" - -PARALLEL_UPDATES = 1 - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up Airly air_quality entity based on a config entry.""" - name = entry.data[CONF_NAME] - - coordinator = hass.data[DOMAIN][entry.entry_id] - - async_add_entities([AirlyAirQuality(coordinator, name)], False) - - -class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): - """Define an Airly air quality.""" - - coordinator: AirlyDataUpdateCoordinator - - def __init__(self, coordinator: AirlyDataUpdateCoordinator, name: str) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_icon = "mdi:blur" - self._attr_name = name - self._attr_unique_id = f"{coordinator.latitude}-{coordinator.longitude}" - - @property - def air_quality_index(self) -> float | None: - """Return the air quality index.""" - return round_state(self.coordinator.data[ATTR_API_CAQI]) - - @property - def particulate_matter_2_5(self) -> float | None: - """Return the particulate matter 2.5 level.""" - return round_state(self.coordinator.data.get(ATTR_API_PM25)) - - @property - def particulate_matter_10(self) -> float | None: - """Return the particulate matter 10 level.""" - return round_state(self.coordinator.data.get(ATTR_API_PM10)) - - @property - def attribution(self) -> str: - """Return the attribution.""" - return ATTRIBUTION - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return { - "identifiers": { - ( - DOMAIN, - f"{self.coordinator.latitude}-{self.coordinator.longitude}", - ) - }, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes.""" - attrs = { - LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], - LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], - LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], - } - if ATTR_API_PM25 in self.coordinator.data: - attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT] - attrs[LABEL_PM_2_5_PERCENT] = round( - self.coordinator.data[ATTR_API_PM25_PERCENT] - ) - if ATTR_API_PM10 in self.coordinator.data: - attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT] - attrs[LABEL_PM_10_PERCENT] = round( - self.coordinator.data[ATTR_API_PM10_PERCENT] - ) - return attrs - - -def round_state(state: float | None) -> float | None: - """Round state.""" - return round(state) if state else state diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index a860a7a1b5a..d79a33a66ab 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -24,16 +24,22 @@ ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" ATTR_API_CAQI_LEVEL: Final = "LEVEL" ATTR_API_HUMIDITY: Final = "HUMIDITY" ATTR_API_PM10: Final = "PM10" -ATTR_API_PM10_LIMIT: Final = "PM10_LIMIT" -ATTR_API_PM10_PERCENT: Final = "PM10_PERCENT" ATTR_API_PM1: Final = "PM1" ATTR_API_PM25: Final = "PM25" -ATTR_API_PM25_LIMIT: Final = "PM25_LIMIT" -ATTR_API_PM25_PERCENT: Final = "PM25_PERCENT" ATTR_API_PRESSURE: Final = "PRESSURE" ATTR_API_TEMPERATURE: Final = "TEMPERATURE" + +ATTR_ADVICE: Final = "advice" +ATTR_DESCRIPTION: Final = "description" ATTR_LABEL: Final = "label" +ATTR_LEVEL: Final = "level" +ATTR_LIMIT: Final = "limit" +ATTR_PERCENT: Final = "percent" ATTR_UNIT: Final = "unit" +ATTR_VALUE: Final = "value" + +SUFFIX_PERCENT: Final = "PERCENT" +SUFFIX_LIMIT: Final = "LIMIT" ATTRIBUTION: Final = "Data provided by Airly" CONF_USE_NEAREST: Final = "use_nearest" @@ -46,32 +52,51 @@ MIN_UPDATE_INTERVAL: Final = 5 NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." SENSOR_TYPES: dict[str, SensorDescription] = { + ATTR_API_CAQI: { + ATTR_LABEL: ATTR_API_CAQI, + ATTR_UNIT: "CAQI", + ATTR_VALUE: round, + }, ATTR_API_PM1: { - ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:blur", ATTR_LABEL: ATTR_API_PM1, ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_API_PM25: { + ATTR_ICON: "mdi:blur", + ATTR_LABEL: "PM2.5", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, + }, + ATTR_API_PM10: { + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM10, + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, }, ATTR_API_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), ATTR_UNIT: PERCENTAGE, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: lambda value: round(value, 1), }, ATTR_API_PRESSURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), ATTR_UNIT: PRESSURE_HPA, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: round, }, ATTR_API_TEMPERATURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), ATTR_UNIT: TEMP_CELSIUS, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_VALUE: lambda value: round(value, 1), }, } diff --git a/homeassistant/components/airly/model.py b/homeassistant/components/airly/model.py index 6109b6e71d9..fe8ad6c929b 100644 --- a/homeassistant/components/airly/model.py +++ b/homeassistant/components/airly/model.py @@ -1,14 +1,15 @@ """Type definitions for Airly integration.""" from __future__ import annotations -from typing import TypedDict +from typing import Callable, TypedDict -class SensorDescription(TypedDict): +class SensorDescription(TypedDict, total=False): """Sensor description class.""" device_class: str | None icon: str | None label: str unit: str - state_class: str + state_class: str | None + value: Callable diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 9c820a02d1b..3f9048dd03e 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,7 +1,7 @@ """Support for the Airly sensor service.""" from __future__ import annotations -from typing import cast +from typing import Any, cast from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -19,15 +19,27 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AirlyDataUpdateCoordinator from .const import ( - ATTR_API_PM1, - ATTR_API_PRESSURE, + ATTR_ADVICE, + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + ATTR_API_PM10, + ATTR_API_PM25, + ATTR_DESCRIPTION, ATTR_LABEL, + ATTR_LEVEL, + ATTR_LIMIT, + ATTR_PERCENT, ATTR_UNIT, + ATTR_VALUE, ATTRIBUTION, DEFAULT_NAME, DOMAIN, MANUFACTURER, SENSOR_TYPES, + SUFFIX_LIMIT, + SUFFIX_PERCENT, ) PARALLEL_UPDATES = 1 @@ -60,26 +72,48 @@ class AirlySensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - description = SENSOR_TYPES[kind] - self._attr_device_class = description[ATTR_DEVICE_CLASS] - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._attr_icon = description[ATTR_ICON] + self._description = description = SENSOR_TYPES[kind] + self._attr_device_class = description.get(ATTR_DEVICE_CLASS) + self._attr_icon = description.get(ATTR_ICON) self._attr_name = f"{name} {description[ATTR_LABEL]}" - self._attr_state_class = description[ATTR_STATE_CLASS] + self._attr_state_class = description.get(ATTR_STATE_CLASS) self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{kind.lower()}" ) - self._attr_unit_of_measurement = description[ATTR_UNIT] + self._attr_unit_of_measurement = description.get(ATTR_UNIT) + self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} self.kind = kind - self._state = None @property def state(self) -> StateType: """Return the state.""" - self._state = self.coordinator.data[self.kind] - if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: - return round(cast(float, self._state)) - return round(cast(float, self._state), 1) + state = self.coordinator.data[self.kind] + return cast(StateType, self._description[ATTR_VALUE](state)) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + if self.kind == ATTR_API_CAQI: + self._attrs[ATTR_LEVEL] = self.coordinator.data[ATTR_API_CAQI_LEVEL] + self._attrs[ATTR_ADVICE] = self.coordinator.data[ATTR_API_ADVICE] + self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[ + ATTR_API_CAQI_DESCRIPTION + ] + if self.kind == ATTR_API_PM25: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_PM25}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"] + ) + if self.kind == ATTR_API_PM10: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_PM10}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"] + ) + return self._attrs @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py deleted file mode 100644 index de059e84aa4..00000000000 --- a/tests/components/airly/test_air_quality.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Test air_quality of Airly integration.""" -from datetime import timedelta - -from airly.exceptions import AirlyError - -from homeassistant.components.air_quality import ATTR_AQI, ATTR_PM_2_5, ATTR_PM_10 -from homeassistant.components.airly.air_quality import ( - ATTRIBUTION, - LABEL_ADVICE, - LABEL_AQI_DESCRIPTION, - LABEL_AQI_LEVEL, - LABEL_PM_2_5_LIMIT, - LABEL_PM_2_5_PERCENT, - LABEL_PM_10_LIMIT, - LABEL_PM_10_PERCENT, -) -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_ENTITY_ID, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - HTTP_INTERNAL_SERVER_ERROR, - STATE_UNAVAILABLE, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from . import API_POINT_URL - -from tests.common import async_fire_time_changed, load_fixture -from tests.components.airly import init_integration - - -async def test_air_quality(hass, aioclient_mock): - """Test states of the air_quality.""" - await init_integration(hass, aioclient_mock) - registry = er.async_get(hass) - - state = hass.states.get("air_quality.home") - assert state - assert state.state == "14" - assert state.attributes.get(ATTR_AQI) == 23 - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(LABEL_ADVICE) == "Great air!" - assert state.attributes.get(ATTR_PM_10) == 19 - assert state.attributes.get(ATTR_PM_2_5) == 14 - assert state.attributes.get(LABEL_AQI_DESCRIPTION) == "Great air here today!" - assert state.attributes.get(LABEL_AQI_LEVEL) == "very low" - assert state.attributes.get(LABEL_PM_2_5_LIMIT) == 25.0 - assert state.attributes.get(LABEL_PM_2_5_PERCENT) == 55 - assert state.attributes.get(LABEL_PM_10_LIMIT) == 50.0 - assert state.attributes.get(LABEL_PM_10_PERCENT) == 37 - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) - assert state.attributes.get(ATTR_ICON) == "mdi:blur" - - entry = registry.async_get("air_quality.home") - assert entry - assert entry.unique_id == "123-456" - - -async def test_availability(hass, aioclient_mock): - """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass, aioclient_mock) - - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "14" - - aioclient_mock.clear_requests() - aioclient_mock.get( - API_POINT_URL, exc=AirlyError(HTTP_INTERNAL_SERVER_ERROR, "Unexpected error") - ) - future = utcnow() + timedelta(minutes=60) - - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.home") - assert state - assert state.state == STATE_UNAVAILABLE - - aioclient_mock.clear_requests() - aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) - future = utcnow() + timedelta(minutes=120) - - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "14" - - -async def test_manual_update_entity(hass, aioclient_mock): - """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass, aioclient_mock) - - call_count = aioclient_mock.call_count - await async_setup_component(hass, "homeassistant", {}) - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["air_quality.home"]}, - blocking=True, - ) - - assert aioclient_mock.call_count == call_count + 1 diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index a20ae6ddd1a..252c01c124a 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -3,10 +3,12 @@ from unittest.mock import patch import pytest +from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.airly import set_update_interval from homeassistant.components.airly.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from . import API_POINT_URL @@ -24,7 +26,7 @@ async def test_async_setup_entry(hass, aioclient_mock): """Test a successful setup entry.""" await init_integration(hass, aioclient_mock) - state = hass.states.get("air_quality.home") + state = hass.states.get("sensor.home_pm2_5") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == "14" @@ -216,3 +218,21 @@ async def test_migrate_device_entry(hass, aioclient_mock, old_identifier): config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123-456")} ) assert device_entry.id == migrated_device_entry.id + + +async def test_remove_air_quality_entities(hass, aioclient_mock): + """Test remove air_quality entities from registry.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + AIR_QUALITY_PLATFORM, + DOMAIN, + "123-456", + suggested_object_id="home", + disabled_by=None, + ) + + await init_integration(hass, aioclient_mock) + + entry = registry.async_get("air_quality.home") + assert entry is None diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 7db3c81f44d..c566702a5b4 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -33,6 +33,16 @@ async def test_sensor(hass, aioclient_mock): await init_integration(hass, aioclient_mock) registry = er.async_get(hass) + state = hass.states.get("sensor.home_caqi") + assert state + assert state.state == "23" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "CAQI" + + entry = registry.async_get("sensor.home_caqi") + assert entry + assert entry.unique_id == "123-456-caqi" + state = hass.states.get("sensor.home_humidity") assert state assert state.state == "92.8" @@ -60,6 +70,36 @@ async def test_sensor(hass, aioclient_mock): assert entry assert entry.unique_id == "123-456-pm1" + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == "14" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + + entry = registry.async_get("sensor.home_pm2_5") + assert entry + assert entry.unique_id == "123-456-pm25" + + state = hass.states.get("sensor.home_pm10") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + + entry = registry.async_get("sensor.home_pm10") + assert entry + assert entry.unique_id == "123-456-pm10" + state = hass.states.get("sensor.home_pressure") assert state assert state.state == "1001" From e14480599bdba8f1e9e0d0b2f3d9e200976385d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Jun 2021 10:23:34 +0200 Subject: [PATCH 580/750] Add value_template support to MQTT number (#52155) --- homeassistant/components/mqtt/number.py | 26 ++++++++++++------- tests/components/mqtt/test_number.py | 33 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index ede9adad51f..f7253541b69 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -11,7 +11,7 @@ from homeassistant.components.number import ( DEFAULT_STEP, NumberEntity, ) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -59,6 +59,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) ), + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), validate_config, @@ -70,28 +71,28 @@ async def async_setup_platform( ): """Set up MQTT number through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(async_add_entities, config) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT number dynamically through MQTT discovery.""" setup = functools.partial( - _async_setup_entity, async_add_entities, config_entry=config_entry + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) await async_setup_entry_helper(hass, number.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - async_add_entities, config, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT number.""" - async_add_entities([MqttNumber(config, config_entry, discovery_data)]) + async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """representation of an MQTT number.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT Number.""" self._config = config self._optimistic = False @@ -100,7 +101,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._current_number = None NumberEntity.__init__(self) - MqttEntity.__init__(self, None, config, config_entry, discovery_data) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod def config_schema(): @@ -111,6 +112,10 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """(Re)Setup the entity.""" self._optimistic = config[CONF_OPTIMISTIC] + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -119,11 +124,14 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + payload = value_template.async_render_with_possible_json_value(payload) try: if payload.isnumeric(): - num_value = int(msg.payload) + num_value = int(payload) else: - num_value = float(msg.payload) + num_value = float(payload) except ValueError: _LOGGER.warning("Payload '%s' is not a Number", msg.payload) return diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index e9dca6f6a5e..8b62d1f9f33 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -81,6 +81,39 @@ async def test_run_number_setup(hass, mqtt_mock): assert state.state == "20.5" +async def test_value_template(hass, mqtt_mock): + """Test that it fetches the given payload with a template.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "value_template": "{{ value_json.val }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, '{"val":10}') + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "10" + + async_fire_mqtt_message(hass, topic, '{"val":20.5}') + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "20.5" + + async def test_run_number_service_optimistic(hass, mqtt_mock): """Test that set_value service works in optimistic mode.""" topic = "test/number" From bfc2995cf84ad3791e8277067e0ff127dc1a52d3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 28 Jun 2021 03:40:51 -0500 Subject: [PATCH 581/750] Update cloudflare test helpers (#52235) --- tests/components/cloudflare/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 0e4e07b91cc..f2eaccab470 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -58,13 +58,21 @@ async def init_integration( *, data: dict = ENTRY_CONFIG, options: dict = ENTRY_OPTIONS, + unique_id: str = MOCK_ZONE, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Cloudflare integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data=data, options=options) + entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options=options, + unique_id=unique_id, + ) entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry From e4fc76ac2c4dd338039d6d8297031cc26eebbff1 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 28 Jun 2021 03:48:18 -0500 Subject: [PATCH 582/750] Add re-authentication support to cloudflare (#51787) --- .../components/cloudflare/__init__.py | 7 ++-- .../components/cloudflare/config_flow.py | 41 ++++++++++++++++++- .../components/cloudflare/strings.json | 7 ++++ .../components/cloudflare/test_config_flow.py | 36 +++++++++++++++- tests/components/cloudflare/test_init.py | 31 +++++++++++++- 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index ed4c54966cf..2d0d3145ead 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -14,7 +14,7 @@ from pycfdns.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -37,9 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: zone_id = await cfupdate.get_zone_id() - except CloudflareAuthenticationException: - _LOGGER.error("API access forbidden. Please reauthenticate") - return False + except CloudflareAuthenticationException as error: + raise ConfigEntryAuthFailed from error except CloudflareConnectionException as error: raise ConfigEntryNotReady from error diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 364700427da..2a369fe65e0 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( @@ -12,9 +13,10 @@ from pycfdns.exceptions import ( import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -85,12 +87,49 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + entry: ConfigEntry | None = None + def __init__(self): """Initialize the Cloudflare config flow.""" self.cloudflare_config = {} self.zones = None self.records = None + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle initiation of re-authentication with Cloudflare.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Cloudflare.""" + errors = {} + + if user_input is not None and self.entry: + _, errors = await self._async_validate_or_error(user_input) + + if not errors: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_TOKEN: user_input[CONF_API_TOKEN], + }, + ) + + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors=errors, + ) + async def async_step_user(self, user_input: dict | None = None): """Handle a flow initiated by the user.""" if self._async_current_entries(): diff --git a/homeassistant/components/cloudflare/strings.json b/homeassistant/components/cloudflare/strings.json index bdadfde4800..31df9a62341 100644 --- a/homeassistant/components/cloudflare/strings.json +++ b/homeassistant/components/cloudflare/strings.json @@ -20,6 +20,12 @@ "data": { "records": "Records" } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Cloudflare account.", + "api_token": "[%key:common::config_flow::data::api_token%]" + } } }, "error": { @@ -28,6 +34,7 @@ "invalid_zone": "Invalid zone" }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "unknown": "[%key:common::config_flow::error::unknown%]" } diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 00dbb5e47df..230f4c3647f 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -6,7 +6,7 @@ from pycfdns.exceptions import ( ) from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -162,3 +162,37 @@ async def test_user_form_single_instance_allowed(hass): ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" + + +async def test_reauth_flow(hass, cfupdate_flow): + """Test the reauthentication configuration flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_TOKEN: "other_token"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert entry.data[CONF_API_TOKEN] == "other_token" + assert entry.data[CONF_ZONE] == ENTRY_CONFIG[CONF_ZONE] + assert entry.data[CONF_RECORDS] == ENTRY_CONFIG[CONF_RECORDS] + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 5a42ca9f09c..ab7dbdab78e 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,8 +1,11 @@ """Test the Cloudflare integration.""" -from pycfdns.exceptions import CloudflareConnectionException +from pycfdns.exceptions import ( + CloudflareAuthenticationException, + CloudflareConnectionException, +) from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from . import ENTRY_CONFIG, init_integration @@ -36,6 +39,30 @@ async def test_async_setup_raises_entry_not_ready(hass, cfupdate): assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_async_setup_raises_entry_auth_failed(hass, cfupdate): + """Test that it throws ConfigEntryAuthFailed when exception occurs during setup.""" + instance = cfupdate.return_value + + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + instance.get_zone_id.side_effect = CloudflareAuthenticationException() + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + async def test_integration_services(hass, cfupdate): """Test integration services.""" instance = cfupdate.return_value From 922b195ebffe35b87ee69e3fda4108cda7990fe0 Mon Sep 17 00:00:00 2001 From: myhomeiot <70070601+myhomeiot@users.noreply.github.com> Date: Mon, 28 Jun 2021 12:10:53 +0300 Subject: [PATCH 583/750] Add hvac_action to Daikin AC (#52035) --- homeassistant/components/daikin/climate.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 7a60cb4c3b2..d17c3fc0d93 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -9,6 +9,10 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -60,6 +64,12 @@ DAIKIN_TO_HA_STATE = { "off": HVAC_MODE_OFF, } +HA_STATE_TO_CURRENT_HVAC = { + HVAC_MODE_COOL: CURRENT_HVAC_COOL, + HVAC_MODE_HEAT: CURRENT_HVAC_HEAT, + HVAC_MODE_OFF: CURRENT_HVAC_OFF, +} + HA_PRESET_TO_DAIKIN = { PRESET_AWAY: "on", PRESET_NONE: "off", @@ -188,6 +198,18 @@ class DaikinClimate(ClimateEntity): """Set new target temperature.""" await self._set(kwargs) + @property + def hvac_action(self): + """Return the current state.""" + ret = HA_STATE_TO_CURRENT_HVAC.get(self.hvac_mode) + if ( + ret in (CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT) + and self._api.device.support_compressor_frequency + and self._api.device.compressor_frequency == 0 + ): + return CURRENT_HVAC_IDLE + return ret + @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" From 486e94e6a5a6dbe1bb1f282e2cb8704f704136f8 Mon Sep 17 00:00:00 2001 From: Brett Date: Mon, 28 Jun 2021 19:21:25 +1000 Subject: [PATCH 584/750] Add "auto" HVAC mode to Advantage Air (#51693) * Add support for myAuto * Small bug fixes for myAutoModeEnabled * Add myauto to test fixture * Refactor hvac_modes using AC_HVAC_MODES --- .../components/advantage_air/climate.py | 19 ++++++++++++------- .../fixtures/advantage_air/getSystemData.json | 5 ++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 60caf15be25..d890fa43207 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -6,6 +6,7 @@ from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -31,9 +32,18 @@ ADVANTAGE_AIR_HVAC_MODES = { "cool": HVAC_MODE_COOL, "vent": HVAC_MODE_FAN_ONLY, "dry": HVAC_MODE_DRY, + "myauto": HVAC_MODE_AUTO, } HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()} +AC_HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, +] + ADVANTAGE_AIR_FAN_MODES = { "auto": FAN_AUTO, "low": FAN_LOW, @@ -43,13 +53,6 @@ ADVANTAGE_AIR_FAN_MODES = { HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} -AC_HVAC_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_DRY, -] ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] @@ -130,6 +133,8 @@ class AdvantageAirAC(AdvantageAirClimateEntity): @property def hvac_modes(self): """Return the supported HVAC modes.""" + if self._ac.get("myAutoModeEnabled"): + return AC_HVAC_MODES + [HVAC_MODE_AUTO] return AC_HVAC_MODES @property diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/fixtures/advantage_air/getSystemData.json index 19dda28fec1..4ed610f9649 100644 --- a/tests/fixtures/advantage_air/getSystemData.json +++ b/tests/fixtures/advantage_air/getSystemData.json @@ -100,7 +100,10 @@ "fan": "low", "filterCleanStatus": 1, "freshAirStatus": "none", - "mode": "cool", + "mode": "myauto", + "myAutoModeCurrentSetMode": "cool", + "myAutoModeEnabled": true, + "myAutoModeIsRunning": true, "myZone": 1, "name": "AC Two", "setTemp": 24, From ab73ce00a01bf456a0f9bc8b17736766698543da Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 28 Jun 2021 12:22:10 +0300 Subject: [PATCH 585/750] Change "Not adding entity" log level to debug (#52240) --- homeassistant/helpers/entity_platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b22fb9ec2d2..5436a01648e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -508,7 +508,7 @@ class EntityPlatform: entity.entity_id = entry.entity_id if entry.disabled: - self.logger.info( + self.logger.debug( "Not adding entity %s because it's disabled", entry.name or entity.name From 3c0a24db507f3025574e371956d418f8093790b9 Mon Sep 17 00:00:00 2001 From: Fabian Zimmermann Date: Mon, 28 Jun 2021 11:35:33 +0200 Subject: [PATCH 586/750] Convert openweathermap dewpoint from kelvin to celcius (#51893) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- .../openweathermap/weather_update_coordinator.py | 7 ++++--- tests/components/openweathermap/test_config_flow.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 4518e3b6bda..98f39290d22 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -18,6 +18,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, ) +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -179,10 +180,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return forecast - @staticmethod - def _fmt_dewpoint(dewpoint): + def _fmt_dewpoint(self, dewpoint): if dewpoint is not None: - return round(dewpoint / 100, 1) + dewpoint = dewpoint - 273.15 + return round(self.hass.config.units.temperature(dewpoint, TEMP_CELSIUS), 1) return None @staticmethod diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index ba1be4afb4c..5225aad83cd 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -187,6 +187,7 @@ def _create_mocked_owm(is_api_online: bool): weather.snow.return_value = [] weather.detailed_status.return_value = "status" weather.weather_code = 803 + weather.dewpoint = 10 mocked_owm.weather_at_coords.return_value.weather = weather From ab24d16e0054c584ff84e39b70b0b793f08865c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Jun 2021 23:46:56 -1000 Subject: [PATCH 587/750] Suppress duplicate mdns discovery from netdisco (#52099) --- homeassistant/components/discovery/__init__.py | 13 ++++++++++--- homeassistant/components/discovery/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/discovery/test_init.py | 12 ++++++------ 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 883958226d8..5b6bb7a5372 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -13,6 +13,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.loader import async_get_zeroconf import homeassistant.util.dt as dt_util DOMAIN = "discovery" @@ -139,6 +140,10 @@ async def async_setup(hass, config): ) zeroconf_instance = await zeroconf.async_get_instance(hass) + # Do not scan for types that have already been converted + # as it will generate excess network traffic for questions + # the zeroconf instance already knows the answers + zeroconf_types = list(await async_get_zeroconf(hass)) async def new_service_found(service, info): """Handle a new service if one is found.""" @@ -187,7 +192,7 @@ async def async_setup(hass, config): """Scan for devices.""" try: results = await hass.async_add_executor_job( - _discover, netdisco, zeroconf_instance + _discover, netdisco, zeroconf_instance, zeroconf_types ) for result in results: @@ -209,11 +214,13 @@ async def async_setup(hass, config): return True -def _discover(netdisco, zeroconf_instance): +def _discover(netdisco, zeroconf_instance, zeroconf_types): """Discover devices.""" results = [] try: - netdisco.scan(zeroconf_instance=zeroconf_instance) + netdisco.scan( + zeroconf_instance=zeroconf_instance, suppress_mdns_types=zeroconf_types + ) for disc in netdisco.discover(): for service in netdisco.get_info(disc): diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index a2d2df1730a..558c727c62c 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.8.3"], + "requirements": ["netdisco==2.9.0"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index 746adfc31ce..087cc1942e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1008,7 +1008,7 @@ nessclient==0.9.15 netdata==0.2.0 # homeassistant.components.discovery -netdisco==2.8.3 +netdisco==2.9.0 # homeassistant.components.nam nettigo-air-monitor==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8661bd0f93e..2913703d6a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.discovery -netdisco==2.8.3 +netdisco==2.9.0 # homeassistant.components.nam nettigo-air-monitor==1.0.0 diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 4dd77c98187..8be837bb16e 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -60,7 +60,7 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG): async def test_unknown_service(hass): """Test that unknown service is ignored.""" - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [("this_service_will_never_be_supported", {"info": "some"})] @@ -73,7 +73,7 @@ async def test_unknown_service(hass): async def test_load_platform(hass): """Test load a platform.""" - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [(SERVICE, SERVICE_INFO)] @@ -89,7 +89,7 @@ async def test_load_platform(hass): async def test_load_component(hass): """Test load a component.""" - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] @@ -109,7 +109,7 @@ async def test_load_component(hass): async def test_ignore_service(hass): """Test ignore service.""" - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] @@ -122,7 +122,7 @@ async def test_ignore_service(hass): async def test_discover_duplicates(hass): """Test load a component.""" - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [ (SERVICE_NO_PLATFORM, SERVICE_INFO), @@ -147,7 +147,7 @@ async def test_discover_config_flow(hass): """Test discovery triggering a config flow.""" discovery_info = {"hello": "world"} - def discover(netdisco, zeroconf_instance): + def discover(netdisco, zeroconf_instance, suppress_mdns_types): """Fake discovery.""" return [("mock-service", discovery_info)] From 4ba5a4f36ede22678d4c6943f61099c91055f806 Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Mon, 28 Jun 2021 13:20:40 +0300 Subject: [PATCH 588/750] Fix unique_id generation for AtwZoneSensors (#51227) --- homeassistant/components/melcloud/sensor.py | 6 ++- .../melcloud/test_atw_zone_sensor.py | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/components/melcloud/test_atw_zone_sensor.py diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 356992ece11..2c59763dc72 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -165,7 +165,11 @@ class AtwZoneSensor(MelDeviceSensor): def __init__(self, api: MelCloudDevice, zone: Zone, measurement, definition): """Initialize the sensor.""" - super().__init__(api, measurement, definition) + if zone.zone_index == 1: + full_measurement = measurement + else: + full_measurement = f"{measurement}-zone-{zone.zone_index}" + super().__init__(api, full_measurement, definition) self._zone = zone self._name_slug = f"{api.name} {zone.name}" diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py new file mode 100644 index 00000000000..ac34a5ccc49 --- /dev/null +++ b/tests/components/melcloud/test_atw_zone_sensor.py @@ -0,0 +1,41 @@ +"""Test the MELCloud ATW zone sensor.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.melcloud.sensor import AtwZoneSensor + + +@pytest.fixture +def mock_device(): + """Mock MELCloud device.""" + with patch("homeassistant.components.melcloud.MelCloudDevice") as mock: + mock.name = "name" + mock.device.serial = 1234 + mock.device.mac = "11:11:11:11:11:11" + yield mock + + +@pytest.fixture +def mock_zone_1(): + """Mock zone 1.""" + with patch("pymelcloud.atw_device.Zone") as mock: + mock.zone_index = 1 + yield mock + + +@pytest.fixture +def mock_zone_2(): + """Mock zone 2.""" + with patch("pymelcloud.atw_device.Zone") as mock: + mock.zone_index = 2 + yield mock + + +def test_zone_unique_ids(mock_device, mock_zone_1, mock_zone_2): + """Test unique id generation correctness.""" + sensor_1 = AtwZoneSensor(mock_device, mock_zone_1, "room_temperature", {}) + assert sensor_1.unique_id == "1234-11:11:11:11:11:11-room_temperature" + + sensor_2 = AtwZoneSensor(mock_device, mock_zone_2, "room_temperature", {}) + assert sensor_2.unique_id == "1234-11:11:11:11:11:11-room_temperature-zone-2" From 583626a74f272435a7165f1ef69e874f426dfd0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Jun 2021 00:49:14 -1000 Subject: [PATCH 589/750] Convert nmap_tracker to be a config flow (#50429) --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/nmap_tracker/__init__.py | 373 +++++++++++++++++- .../components/nmap_tracker/config_flow.py | 200 ++++++++++ .../components/nmap_tracker/const.py | 14 + .../components/nmap_tracker/device_tracker.py | 250 +++++++----- .../components/nmap_tracker/manifest.json | 14 +- .../components/nmap_tracker/strings.json | 38 ++ .../nmap_tracker/translations/en.json | 38 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 7 + tests/components/nmap_tracker/__init__.py | 1 + .../nmap_tracker/test_config_flow.py | 285 +++++++++++++ 14 files changed, 1124 insertions(+), 105 deletions(-) create mode 100644 homeassistant/components/nmap_tracker/config_flow.py create mode 100644 homeassistant/components/nmap_tracker/const.py create mode 100644 homeassistant/components/nmap_tracker/strings.json create mode 100644 homeassistant/components/nmap_tracker/translations/en.json create mode 100644 tests/components/nmap_tracker/__init__.py create mode 100644 tests/components/nmap_tracker/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a58af4a7836..8ba4b81a0b8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -691,6 +691,7 @@ omit = homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* + homeassistant/components/nmap_tracker/__init__.py homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py homeassistant/components/notion/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a1f44cb1c7..5352cc8e675 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -330,6 +330,7 @@ homeassistant/components/nextcloud/* @meichthys homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole +homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index da699caaa73..381813a3b49 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1 +1,372 @@ -"""The nmap_tracker component.""" +"""The Nmap Tracker integration.""" +from __future__ import annotations + +import asyncio +import contextlib +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging + +import aiohttp +from getmac import get_mac_address +from mac_vendor_lookup import AsyncMacLookup +from nmap import PortScanner, PortScannerError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DOMAIN, + NMAP_TRACKED_DEVICES, + PLATFORMS, + TRACKER_SCAN_INTERVAL, +) + +# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n' +NMAP_TRANSIENT_FAILURE = "Assertion failed: htn.toclock_running == true" +MAX_SCAN_ATTEMPTS = 16 +OFFLINE_SCANS_TO_MARK_UNAVAILABLE = 3 + + +def short_hostname(hostname): + """Return the first part of the hostname.""" + if hostname is None: + return None + return hostname.split(".")[0] + + +def human_readable_name(hostname, vendor, mac_address): + """Generate a human readable name.""" + if hostname: + return short_hostname(hostname) + if vendor: + return f"{vendor} {mac_address[-8:]}" + return f"Nmap Tracker {mac_address}" + + +@dataclass +class NmapDevice: + """Class for keeping track of an nmap tracked device.""" + + mac_address: str + hostname: str + name: str + ipv4: str + manufacturer: str + reason: str + last_update: datetime.datetime + offline_scans: int + + +class NmapTrackedDevices: + """Storage class for all nmap trackers.""" + + def __init__(self) -> None: + """Initialize the data.""" + self.tracked: dict = {} + self.ipv4_last_mac: dict = {} + self.config_entry_owner: dict = {} + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nmap Tracker from a config entry.""" + domain_data = hass.data.setdefault(DOMAIN, {}) + devices = domain_data.setdefault(NMAP_TRACKED_DEVICES, NmapTrackedDevices()) + scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) + await scanner.async_setup() + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + _async_untrack_devices(hass, entry) + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@callback +def _async_untrack_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove tracking for devices owned by this config entry.""" + devices = hass.data[DOMAIN][NMAP_TRACKED_DEVICES] + remove_mac_addresses = [ + mac_address + for mac_address, entry_id in devices.config_entry_owner.items() + if entry_id == entry.entry_id + ] + for mac_address in remove_mac_addresses: + if device := devices.tracked.pop(mac_address, None): + devices.ipv4_last_mac.pop(device.ipv4, None) + del devices.config_entry_owner[mac_address] + + +def signal_device_update(mac_address) -> str: + """Signal specific per nmap tracker entry to signal updates in device.""" + return f"{DOMAIN}-device-update-{mac_address}" + + +class NmapDeviceScanner: + """This class scans for devices using nmap.""" + + def __init__(self, hass, entry, devices): + """Initialize the scanner.""" + self.devices = devices + self.home_interval = None + + self._hass = hass + self._entry = entry + + self._scan_lock = None + self._stopping = False + self._scanner = None + + self._entry_id = entry.entry_id + self._hosts = None + self._options = None + self._exclude = None + + self._finished_first_scan = False + self._last_results = [] + self._mac_vendor_lookup = None + + async def async_setup(self): + """Set up the tracker.""" + config = self._entry.options + self._hosts = cv.ensure_list_csv(config[CONF_HOSTS]) + self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE]) + self._options = config[CONF_OPTIONS] + self.home_interval = timedelta( + minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) + ) + self._scan_lock = asyncio.Lock() + if self._hass.state == CoreState.running: + await self._async_start_scanner() + return + + self._entry.async_on_unload( + self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner + ) + ) + + @property + def signal_device_new(self) -> str: + """Signal specific per nmap tracker entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._entry_id}" + + @property + def signal_device_missing(self) -> str: + """Signal specific per nmap tracker entry to signal a missing device.""" + return f"{DOMAIN}-device-missing-{self._entry_id}" + + @callback + def _async_get_vendor(self, mac_address): + """Lookup the vendor.""" + oui = self._mac_vendor_lookup.sanitise(mac_address)[:6] + return self._mac_vendor_lookup.prefixes.get(oui) + + @callback + def _async_stop(self): + """Stop the scanner.""" + self._stopping = True + + async def _async_start_scanner(self, *_): + """Start the scanner.""" + self._entry.async_on_unload(self._async_stop) + self._entry.async_on_unload( + async_track_time_interval( + self._hass, + self._async_scan_devices, + timedelta(seconds=TRACKER_SCAN_INTERVAL), + ) + ) + self._mac_vendor_lookup = AsyncMacLookup() + with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): + # We don't care of this fails since its only + # improves the data when we don't have it from nmap + await self._mac_vendor_lookup.load_vendors() + self._hass.async_create_task(self._async_scan_devices()) + + def _build_options(self): + """Build the command line and strip out last results that do not need to be updated.""" + options = self._options + if self.home_interval: + boundary = dt_util.now() - self.home_interval + last_results = [ + device for device in self._last_results if device.last_update > boundary + ] + if last_results: + exclude_hosts = self._exclude + [device.ipv4 for device in last_results] + else: + exclude_hosts = self._exclude + else: + last_results = [] + exclude_hosts = self._exclude + if exclude_hosts: + options += f" --exclude {','.join(exclude_hosts)}" + # Report reason + if "--reason" not in options: + options += " --reason" + # Report down hosts + if "-v" not in options: + options += " -v" + self._last_results = last_results + return options + + async def _async_scan_devices(self, *_): + """Scan devices and dispatch.""" + if self._scan_lock.locked(): + _LOGGER.debug( + "Nmap scanning is taking longer than the scheduled interval: %s", + TRACKER_SCAN_INTERVAL, + ) + return + + async with self._scan_lock: + try: + await self._async_run_nmap_scan() + except PortScannerError as ex: + _LOGGER.error("Nmap scanning failed: %s", ex) + + if not self._finished_first_scan: + self._finished_first_scan = True + await self._async_mark_missing_devices_as_not_home() + + async def _async_mark_missing_devices_as_not_home(self): + # After all config entries have finished their first + # scan we mark devices that were not found as not_home + # from unavailable + registry = er.async_get(self._hass) + now = dt_util.now() + for entry in registry.entities.values(): + if entry.config_entry_id != self._entry_id: + continue + if entry.unique_id not in self.devices.tracked: + self.devices.config_entry_owner[entry.unique_id] = self._entry_id + self.devices.tracked[entry.unique_id] = NmapDevice( + entry.unique_id, + None, + entry.original_name, + None, + self._async_get_vendor(entry.unique_id), + "Device not found in initial scan", + now, + 1, + ) + async_dispatcher_send( + self._hass, self.signal_device_missing, entry.unique_id + ) + + def _run_nmap_scan(self): + """Run nmap and return the result.""" + options = self._build_options() + if not self._scanner: + self._scanner = PortScanner() + _LOGGER.debug("Scanning %s with args: %s", self._hosts, options) + for attempt in range(MAX_SCAN_ATTEMPTS): + try: + result = self._scanner.scan( + hosts=" ".join(self._hosts), + arguments=options, + timeout=TRACKER_SCAN_INTERVAL * 10, + ) + break + except PortScannerError as ex: + if attempt < (MAX_SCAN_ATTEMPTS - 1) and NMAP_TRANSIENT_FAILURE in str( + ex + ): + _LOGGER.debug("Nmap saw transient error %s", NMAP_TRANSIENT_FAILURE) + continue + raise + _LOGGER.debug( + "Finished scanning %s with args: %s", + self._hosts, + options, + ) + return result + + @callback + def _async_increment_device_offline(self, ipv4, reason): + """Mark an IP offline.""" + if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)): + return + if not (device := self.devices.tracked.get(formatted_mac)): + # Device was unloaded + return + device.offline_scans += 1 + if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE: + return + device.reason = reason + async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False) + del self.devices.ipv4_last_mac[ipv4] + + async def _async_run_nmap_scan(self): + """Scan the network for devices and dispatch events.""" + result = await self._hass.async_add_executor_job(self._run_nmap_scan) + if self._stopping: + return + + devices = self.devices + entry_id = self._entry_id + now = dt_util.now() + for ipv4, info in result["scan"].items(): + status = info["status"] + reason = status["reason"] + if status["state"] != "up": + self._async_increment_device_offline(ipv4, reason) + continue + # Mac address only returned if nmap ran as root + mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) + if mac is None: + self._async_increment_device_offline(ipv4, "No MAC address found") + _LOGGER.info("No MAC address found for %s", ipv4) + continue + + hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 + + formatted_mac = format_mac(mac) + if ( + devices.config_entry_owner.setdefault(formatted_mac, entry_id) + != entry_id + ): + continue + + vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) + name = human_readable_name(hostname, vendor, mac) + device = NmapDevice( + formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 + ) + new = formatted_mac not in devices.tracked + + devices.tracked[formatted_mac] = device + devices.ipv4_last_mac[ipv4] = formatted_mac + self._last_results.append(device) + + if new: + async_dispatcher_send(self._hass, self.signal_device_new, formatted_mac) + else: + async_dispatcher_send( + self._hass, signal_device_update(formatted_mac), True + ) diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py new file mode 100644 index 00000000000..942689ad575 --- /dev/null +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -0,0 +1,200 @@ +"""Config flow for Nmap Tracker integration.""" +from __future__ import annotations + +from ipaddress import ip_address, ip_network, summarize_address_range +from typing import Any + +import ifaddr +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.util import get_local_ip + +from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN + +DEFAULT_NETWORK_PREFIX = 24 + + +def get_network(): + """Search adapters for the network.""" + adapters = ifaddr.get_adapters() + local_ip = get_local_ip() + network_prefix = ( + get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX + ) + return str(ip_network(f"{local_ip}/{network_prefix}", False)) + + +def get_ip_prefix_from_adapters(local_ip, adapters): + """Find the network prefix for an adapter.""" + for adapter in adapters: + for ip_cfg in adapter.ips: + if local_ip == ip_cfg.ip: + return ip_cfg.network_prefix + + +def _normalize_ips_and_network(hosts_str): + """Check if a list of hosts are all ips or ip networks.""" + + normalized_hosts = [] + hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""] + + for host in sorted(hosts): + try: + start, end = host.split("-", 1) + if "." not in end: + ip_1, ip_2, ip_3, _ = start.split(".", 3) + end = ".".join([ip_1, ip_2, ip_3, end]) + summarize_address_range(ip_address(start), ip_address(end)) + except ValueError: + pass + else: + normalized_hosts.append(host) + continue + + try: + ip_addr = ip_address(host) + except ValueError: + pass + else: + normalized_hosts.append(str(ip_addr)) + continue + + try: + network = ip_network(host) + except ValueError: + return None + else: + normalized_hosts.append(str(network)) + + return normalized_hosts + + +def normalize_input(user_input): + """Validate hosts and exclude are valid.""" + errors = {} + normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + if not normalized_hosts: + errors[CONF_HOSTS] = "invalid_hosts" + else: + user_input[CONF_HOSTS] = ",".join(normalized_hosts) + + normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE]) + if normalized_exclude is None: + errors[CONF_EXCLUDE] = "invalid_hosts" + else: + user_input[CONF_EXCLUDE] = ",".join(normalized_exclude) + + return errors + + +async def _async_build_schema_with_user_input(hass, user_input): + hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) + exclude = user_input.get( + CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) + ) + return vol.Schema( + { + vol.Required(CONF_HOSTS, default=hosts): str, + vol.Required( + CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) + ): int, + vol.Optional(CONF_EXCLUDE, default=exclude): str, + vol.Optional( + CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) + ): str, + } + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for homekit.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + errors = {} + if user_input is not None: + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.options + ) + + return self.async_show_form( + step_id="init", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options + ), + errors=errors, + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nmap Tracker.""" + + VERSION = 1 + + def __init__(self): + """Initialize config flow.""" + self.options = {} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + errors = normalize_input(user_input) + self.options.update(user_input) + + if not errors: + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", + data={}, + options=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=await _async_build_schema_with_user_input( + self.hass, self.options + ), + errors=errors, + ) + + def _async_is_unique_host_list(self, user_input): + hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) + for entry in self._async_current_entries(): + if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts: + return False + return True + + async def async_step_import(self, user_input=None): + """Handle import from yaml.""" + if not self._async_is_unique_host_list(user_input): + return self.async_abort(reason="already_configured") + + normalize_input(user_input) + + return self.async_create_entry( + title=f"Nmap Tracker {user_input[CONF_HOSTS]}", data={}, options=user_input + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py new file mode 100644 index 00000000000..e71c2d58bbb --- /dev/null +++ b/homeassistant/components/nmap_tracker/const.py @@ -0,0 +1,14 @@ +"""The Nmap Tracker integration.""" + +DOMAIN = "nmap_tracker" + +PLATFORMS = ["device_tracker"] + +NMAP_TRACKED_DEVICES = "nmap_tracked_devices" + +# Interval in minutes to exclude devices from a scan while they are home +CONF_HOME_INTERVAL = "home_interval" +CONF_OPTIONS = "scan_options" +DEFAULT_OPTIONS = "-F --host-timeout 5s" + +TRACKER_SCAN_INTERVAL = 120 diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 69c65873e51..24e0d3d8e26 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,29 +1,28 @@ """Support for scanning a network with nmap.""" -from collections import namedtuple -from datetime import timedelta -import logging -from getmac import get_mac_address -from nmap import PortScanner, PortScannerError +import logging +from typing import Callable + import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import NmapDeviceScanner, short_hostname, signal_device_update +from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN _LOGGER = logging.getLogger(__name__) -# Interval in minutes to exclude devices from a scan while they are home -CONF_HOME_INTERVAL = "home_interval" -CONF_OPTIONS = "scan_options" -DEFAULT_OPTIONS = "-F --host-timeout 5s" - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): cv.ensure_list, @@ -34,100 +33,153 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_scanner(hass, config): +async def async_get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - return NmapDeviceScanner(config[DOMAIN]) + validated_config = config[DEVICE_TRACKER_DOMAIN] - -Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) - - -class NmapDeviceScanner(DeviceScanner): - """This class scans for devices using nmap.""" - - exclude = [] - - def __init__(self, config): - """Initialize the scanner.""" - self.last_results = [] - - self.hosts = config[CONF_HOSTS] - self.exclude = config[CONF_EXCLUDE] - minutes = config[CONF_HOME_INTERVAL] - self._options = config[CONF_OPTIONS] - self.home_interval = timedelta(minutes=minutes) - - _LOGGER.debug("Scanner initialized") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - _LOGGER.debug("Nmap last results %s", self.last_results) - - return [device.mac for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - filter_named = [ - result.name for result in self.last_results if result.mac == device - ] - - if filter_named: - return filter_named[0] - return None - - def get_extra_attributes(self, device): - """Return the IP of the given device.""" - filter_ip = next( - (result.ip for result in self.last_results if result.mac == device), None + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), + CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], + CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), + CONF_OPTIONS: validated_config[CONF_OPTIONS], + }, ) - return {"ip": filter_ip} + ) - def _update_info(self): - """Scan the network for devices. + _LOGGER.warning( + "Your Nmap Tracker configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + ) - Returns boolean if scanning successful. - """ - _LOGGER.debug("Scanning") - scanner = PortScanner() +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up device tracker for Nmap Tracker component.""" + nmap_tracker = hass.data[DOMAIN][entry.entry_id] - options = self._options + @callback + def device_new(mac_address): + """Signal a new device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)]) - if self.home_interval: - boundary = dt_util.now() - self.home_interval - last_results = [ - device for device in self.last_results if device.last_update > boundary - ] - if last_results: - exclude_hosts = self.exclude + [device.ip for device in last_results] - else: - exclude_hosts = self.exclude - else: - last_results = [] - exclude_hosts = self.exclude - if exclude_hosts: - options += f" --exclude {','.join(exclude_hosts)}" + @callback + def device_missing(mac_address): + """Signal a missing device.""" + async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)]) - try: - result = scanner.scan(hosts=" ".join(self.hosts), arguments=options) - except PortScannerError: - return False + entry.async_on_unload( + async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new) + ) + entry.async_on_unload( + async_dispatcher_connect( + hass, nmap_tracker.signal_device_missing, device_missing + ) + ) - now = dt_util.now() - for ipv4, info in result["scan"].items(): - if info["status"]["state"] != "up": - continue - name = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - # Mac address only returned if nmap ran as root - mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) - if mac is None: - _LOGGER.info("No MAC address found for %s", ipv4) - continue - last_results.append(Device(mac.upper(), name, ipv4, now)) - self.last_results = last_results +class NmapTrackerEntity(ScannerEntity): + """An Nmap Tracker entity.""" - _LOGGER.debug("nmap scan successful") - return True + def __init__( + self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool + ) -> None: + """Initialize an nmap tracker entity.""" + self._mac_address = mac_address + self._nmap_tracker = nmap_tracker + self._tracked = self._nmap_tracker.devices.tracked + self._active = active + + @property + def _device(self) -> bool: + """Get latest device state.""" + return self._tracked[self._mac_address] + + @property + def is_connected(self) -> bool: + """Return device status.""" + return self._active + + @property + def name(self) -> str: + """Return device name.""" + return self._device.name + + @property + def unique_id(self) -> str: + """Return device unique id.""" + return self._mac_address + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ipv4 + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return short_hostname(self._device.hostname) + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def device_info(self): + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, + "default_manufacturer": self._device.manufacturer, + "default_name": self.name, + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def icon(self): + """Return device icon.""" + return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" + + @callback + def async_process_update(self, online: bool) -> None: + """Update device.""" + self._active = online + + @property + def extra_state_attributes(self): + """Return the attributes.""" + return { + "last_time_reachable": self._device.last_update.isoformat( + timespec="seconds" + ), + "reason": self._device.reason, + } + + @callback + def async_on_demand_update(self, online: bool): + """Update state.""" + self.async_process_update(online) + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_device_update(self._mac_address), + self.async_on_demand_update, + ) + ) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 9f81c0facaf..3d0f9d9b014 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -2,7 +2,13 @@ "domain": "nmap_tracker", "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [], - "iot_class": "local_polling" -} + "requirements": [ + "python-nmap==0.6.4", + "getmac==0.8.2", + "ifaddr==0.1.7", + "mac-vendor-lookup==0.1.11" + ], + "codeowners": ["@bdraco"], + "iot_class": "local_polling", + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json new file mode 100644 index 00000000000..a1e04b681cd --- /dev/null +++ b/homeassistant/components/nmap_tracker/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Nmap Tracker", + "options": { + "step": { + "init": { + "description": "[%key:component::nmap_tracker::config::step::user::description%]", + "data": { + "hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]", + "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", + "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", + "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]" + } + } + }, + "error": { + "invalid_hosts": "[%key:component::nmap_tracker::config::error::invalid_hosts%]" + } + }, + "config": { + "step": { + "user": { + "description":"Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32).", + "data": { + "hosts": "Network addresses (comma seperated) to scan", + "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", + "exclude": "Network addresses (comma seperated) to exclude from scanning", + "scan_options": "Raw configurable scan options for Nmap" + } + } + }, + "error": { + "invalid_hosts": "Invalid Hosts" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json new file mode 100644 index 00000000000..ed37a6a5410 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "invalid_hosts": "Invalid Hosts" + }, + "step": { + "user": { + "data": { + "exclude": "Network addresses (comma seperated) to exclude from scanning", + "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", + "hosts": "Network addresses (comma seperated) to scan", + "scan_options": "Raw configurable scan options for Nmap" + }, + "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Invalid Hosts" + }, + "step": { + "init": { + "data": { + "exclude": "Network addresses (comma seperated) to exclude from scanning", + "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", + "hosts": "Network addresses (comma seperated) to scan", + "scan_options": "Raw configurable scan options for Nmap" + }, + "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 69886e370f7..48545ff0e06 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -174,6 +174,7 @@ FLOWS = [ "netatmo", "nexia", "nightscout", + "nmap_tracker", "notion", "nuheat", "nuki", diff --git a/requirements_all.txt b/requirements_all.txt index 087cc1942e3..7f8dba6c6e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,6 +831,7 @@ ibmiotf==0.3.4 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.iglo @@ -932,6 +933,9 @@ lw12==0.9.2 # homeassistant.components.lyft lyft_rides==0.2 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -1857,7 +1861,7 @@ python-mystrom==1.1.2 python-nest==4.1.0 # homeassistant.components.nmap_tracker -python-nmap==0.6.1 +python-nmap==0.6.4 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2913703d6a5..6381470a6cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -475,6 +475,7 @@ iaqualink==0.3.90 icmplib==3.0 # homeassistant.components.network +# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.influxdb @@ -516,6 +517,9 @@ logi_circle==0.2.2 # homeassistant.components.luftdaten luftdaten==0.6.5 +# homeassistant.components.nmap_tracker +mac-vendor-lookup==0.1.11 + # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -1026,6 +1030,9 @@ python-miio==0.5.6 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.nmap_tracker +python-nmap==0.6.4 + # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/tests/components/nmap_tracker/__init__.py b/tests/components/nmap_tracker/__init__.py new file mode 100644 index 00000000000..f5e0c85df31 --- /dev/null +++ b/tests/components/nmap_tracker/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nmap Tracker integration.""" diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py new file mode 100644 index 00000000000..1556dee58d9 --- /dev/null +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -0,0 +1,285 @@ +"""Test the Nmap Tracker config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.nmap_tracker.const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DOMAIN, +) +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +from homeassistant.core import CoreState, HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] +) +async def test_form(hass: HomeAssistant, hosts: str) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == f"Nmap Tracker {hosts}" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: hosts, + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_range(hass: HomeAssistant) -> None: + """Test we get the form and can take an ip range.""" + 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.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Nmap Tracker 192.168.0.5-12" + assert result2["data"] == {} + assert result2["options"] == { + CONF_HOSTS: "192.168.0.5-12", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_hosts(hass: HomeAssistant) -> None: + """Test invalid hosts passed in.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "not an ip block", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} + + +async def test_form_already_configured(hass: HomeAssistant) -> None: + """Test duplicate host list.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_form_invalid_excludes(hass: HomeAssistant) -> None: + """Test invalid excludes passed in.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTS: "3.3.3.3", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "not an exclude", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test we can edit options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.1.0/24", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + hass.state = CoreState.stopped + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", + CONF_HOME_INTERVAL: 5, + CONF_OPTIONS: "-sn", + CONF_EXCLUDE: "4.4.4.4,5.5.5.5", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.nmap_tracker.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "Nmap Tracker 1.2.3.4/20" + assert result["data"] == {} + assert result["options"] == { + CONF_HOSTS: "1.2.3.4/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4,6.4.3.2", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_aborts_if_matching(hass: HomeAssistant) -> None: + """Test we can import from yaml.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4", + }, + ) + config_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOSTS: "192.168.0.0/20", + CONF_HOME_INTERVAL: 3, + CONF_OPTIONS: DEFAULT_OPTIONS, + CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 90e9216e9ac0b80041ef76952a4e40cf8ffb44a8 Mon Sep 17 00:00:00 2001 From: billsq Date: Mon, 28 Jun 2021 03:54:03 -0700 Subject: [PATCH 590/750] Add support for overriding SMTP recipient(s) in a service call (#47611) --- homeassistant/components/smtp/notify.py | 13 ++++++--- tests/components/smtp/test_notify.py | 35 +++++++++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index f7d3415525d..29f0eb777ba 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, + ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, @@ -182,7 +183,11 @@ class MailNotificationService(BaseNotificationService): msg = _build_text_msg(message) msg["Subject"] = subject - msg["To"] = ",".join(self.recipients) + + recipients = kwargs.get(ATTR_TARGET) + if not recipients: + recipients = self.recipients + msg["To"] = recipients if isinstance(recipients, str) else ",".join(recipients) if self._sender_name: msg["From"] = f"{self._sender_name} <{self._sender}>" else: @@ -191,14 +196,14 @@ class MailNotificationService(BaseNotificationService): msg["Date"] = email.utils.format_datetime(dt_util.now()) msg["Message-Id"] = email.utils.make_msgid() - return self._send_email(msg) + return self._send_email(msg, recipients) - def _send_email(self, msg): + def _send_email(self, msg, recipients): """Send the message.""" mail = self.connect() for _ in range(self.tries): try: - mail.sendmail(self._sender, self.recipients, msg.as_string()) + mail.sendmail(self._sender, recipients, msg.as_string()) break except smtplib.SMTPServerDisconnected: _LOGGER.warning( diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 46f8d0efd5f..5af1e5fcdbc 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -16,9 +16,9 @@ from homeassistant.setup import async_setup_component class MockSMTP(MailNotificationService): """Test SMTP object that doesn't need a working server.""" - def _send_email(self, msg): - """Just return string for testing.""" - return msg.as_string() + def _send_email(self, msg, recipients): + """Just return msg string and recipients for testing.""" + return msg.as_string(), recipients async def test_reload_notify(hass): @@ -140,7 +140,7 @@ def test_send_message(message_data, data, content_type, hass, message): """Verify if we can send messages of all types correctly.""" sample_email = "" with patch("email.utils.make_msgid", return_value=sample_email): - result = message.send_message(message_data, data=data) + result, _ = message.send_message(message_data, data=data) assert content_type in result @@ -162,5 +162,30 @@ def test_send_text_message(hass, message): sample_email = "" message_data = "Test msg" with patch("email.utils.make_msgid", return_value=sample_email): - result = message.send_message(message_data) + result, _ = message.send_message(message_data) assert re.search(expected, result) + + +@pytest.mark.parametrize( + "target", + [ + None, + "target@example.com", + ], + ids=[ + "Verify we can send email to default recipient.", + "Verify email recipient can be overwritten by target arg.", + ], +) +def test_send_target_message(target, hass, message): + """Verify if we can send email to correct recipient.""" + sample_email = "" + message_data = "Test msg" + with patch("email.utils.make_msgid", return_value=sample_email): + if not target: + expected_recipient = ["recip1@example.com", "testrecip@test.com"] + else: + expected_recipient = target + + _, recipient = message.send_message(message_data, target=target) + assert recipient == expected_recipient From 594bcbcf7a3edeba725397273e0a06b7f4e71ebf Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 28 Jun 2021 06:57:07 -0400 Subject: [PATCH 591/750] Fix timezones in Environment Canada hourly forecasts (#51917) --- .../components/environment_canada/weather.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 5301843fc0a..a4a8a02cee9 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -232,19 +232,20 @@ def get_forecast(ec_data, forecast_type): ) elif forecast_type == "hourly": - hours = ec_data.hourly_forecasts - for hour in range(0, 24): + for hour in ec_data.hourly_forecasts: forecast_array.append( { - ATTR_FORECAST_TIME: dt.as_local( - datetime.datetime.strptime(hours[hour]["period"], "%Y%m%d%H%M") - ).isoformat(), - ATTR_FORECAST_TEMP: int(hours[hour]["temperature"]), + ATTR_FORECAST_TIME: datetime.datetime.strptime( + hour["period"], "%Y%m%d%H%M%S" + ) + .replace(tzinfo=dt.UTC) + .isoformat(), + ATTR_FORECAST_TEMP: int(hour["temperature"]), ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(hours[hour]["icon_code"]) + int(hour["icon_code"]) ), ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( - hours[hour]["precip_probability"] + hour["precip_probability"] ), } ) From 3e1d32f4e004f8e056dccea978252ce5e9827585 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 28 Jun 2021 13:43:45 +0200 Subject: [PATCH 592/750] ESPHome Climate add preset, custom preset, custom fan mode (#52133) * ESPHome Climate add preset, custom preset, custom fan mode * Fix copy paste error * Bump aioesphomeapi to 3.0.0 * Bump aioesphomeapi to 3.0.1 * Persist api version to prevent exception for offline devices --- homeassistant/components/esphome/__init__.py | 6 ++ homeassistant/components/esphome/climate.py | 65 ++++++++++++++----- homeassistant/components/esphome/cover.py | 2 +- .../components/esphome/entry_data.py | 12 +++- homeassistant/components/esphome/fan.py | 2 +- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 72 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7783047a662..2490c0bdbd3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -10,6 +10,7 @@ from typing import Generic, TypeVar from aioesphomeapi import ( APIClient, APIConnectionError, + APIVersion, DeviceInfo as EsphomeDeviceInfo, EntityInfo, EntityState, @@ -206,6 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nonlocal device_id try: entry_data.device_info = await cli.device_info() + entry_data.api_version = cli.api_version entry_data.available = True device_id = await _async_setup_device_registry( hass, entry, entry_data.device_info @@ -785,6 +787,10 @@ class EsphomeBaseEntity(Entity): def _entry_data(self) -> RuntimeEntryData: return self.hass.data[DOMAIN][self._entry_id] + @property + def _api_version(self) -> APIVersion: + return self._entry_data.api_version + @property def _static_info(self) -> EntityInfo: # Check if value is in info database. Use a single lookup. diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 15edcdd8150..f7ebccc8434 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -6,6 +6,7 @@ from aioesphomeapi import ( ClimateFanMode, ClimateInfo, ClimateMode, + ClimatePreset, ClimateState, ClimateSwingMode, ) @@ -30,14 +31,21 @@ from homeassistant.components.climate.const import ( FAN_MIDDLE, FAN_OFF, FAN_ON, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_ACTIVITY, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, @@ -80,11 +88,12 @@ async def async_setup_entry(hass, entry, async_add_entities): _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper( { ClimateMode.OFF: HVAC_MODE_OFF, - ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, + ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, ClimateMode.HEAT: HVAC_MODE_HEAT, ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, ClimateMode.DRY: HVAC_MODE_DRY, + ClimateMode.AUTO: HVAC_MODE_AUTO, } ) _CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper( @@ -118,6 +127,18 @@ _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper( ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } ) +_PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper( + { + ClimatePreset.NONE: PRESET_NONE, + ClimatePreset.HOME: PRESET_HOME, + ClimatePreset.AWAY: PRESET_AWAY, + ClimatePreset.BOOST: PRESET_BOOST, + ClimatePreset.COMFORT: PRESET_COMFORT, + ClimatePreset.ECO: PRESET_ECO, + ClimatePreset.SLEEP: PRESET_SLEEP, + ClimatePreset.ACTIVITY: PRESET_ACTIVITY, + } +) class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @@ -155,17 +176,20 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): ] @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return [ _FAN_MODES.from_esphome(mode) for mode in self._static_info.supported_fan_modes - ] + ] + self._static_info.supported_custom_fan_modes @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return preset modes.""" - return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] + return [ + _PRESETS.from_esphome(preset) + for preset in self._static_info.supported_presets_compat(self._api_version) + ] + self._static_info.supported_custom_presets @property def swing_modes(self): @@ -199,7 +223,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): features |= SUPPORT_TARGET_TEMPERATURE_RANGE else: features |= SUPPORT_TARGET_TEMPERATURE - if self._static_info.supports_away: + if self.preset_modes: features |= SUPPORT_PRESET_MODE if self._static_info.supported_fan_modes: features |= SUPPORT_FAN_MODE @@ -226,12 +250,16 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @esphome_state_property def fan_mode(self) -> str | None: """Return current fan setting.""" - return _FAN_MODES.from_esphome(self._state.fan_mode) + return self._state.custom_fan_mode or _FAN_MODES.from_esphome( + self._state.fan_mode + ) @esphome_state_property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return current preset mode.""" - return PRESET_AWAY if self._state.away else PRESET_HOME + return self._state.custom_preset or _PRESETS.from_esphome( + self._state.preset_compat(self._api_version) + ) @esphome_state_property def swing_mode(self) -> str | None: @@ -277,16 +305,23 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): key=self._static_info.key, mode=_CLIMATE_MODES.from_hass(hvac_mode) ) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - away = preset_mode == PRESET_AWAY - await self._client.climate_command(key=self._static_info.key, away=away) + kwargs = {} + if preset_mode in self._static_info.supported_custom_presets: + kwargs["custom_preset"] = preset_mode + else: + kwargs["preset"] = _PRESETS.from_hass(preset_mode) + await self._client.climate_command(key=self._static_info.key, **kwargs) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - await self._client.climate_command( - key=self._static_info.key, fan_mode=_FAN_MODES.from_hass(fan_mode) - ) + kwargs = {} + if fan_mode in self._static_info.supported_custom_fan_modes: + kwargs["custom_fan_mode"] = fan_mode + else: + kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode) + await self._client.climate_command(key=self._static_info.key, **kwargs) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 3f4bd29198c..3064f827d7f 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -74,7 +74,7 @@ class EsphomeCover(EsphomeEntity, CoverEntity): def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change - return self._state.is_closed(self._client.api_version) + return self._state.is_closed(self._api_version) @esphome_state_property def is_opening(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c5d36e3a68d..dc964d0eb2e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, + APIVersion, BinarySensorInfo, CameraInfo, ClimateInfo, @@ -67,6 +68,7 @@ class RuntimeEntryData: services: dict[int, UserService] = attr.ib(factory=dict) available: bool = attr.ib(default=False) device_info: DeviceInfo | None = attr.ib(default=None) + api_version: APIVersion = attr.ib(factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list) disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list) loaded_platforms: set[str] = attr.ib(factory=set) @@ -141,6 +143,10 @@ class RuntimeEntryData: self.device_info = _attr_obj_from_dict( DeviceInfo, **restored.pop("device_info") ) + self.api_version = _attr_obj_from_dict( + APIVersion, **restored.pop("api_version", {}) + ) + infos = [] for comp_type, restored_infos in restored.items(): if comp_type not in COMPONENT_TYPE_TO_INFO: @@ -155,7 +161,11 @@ class RuntimeEntryData: async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" - store_data = {"device_info": attr.asdict(self.device_info), "services": []} + store_data = { + "device_info": attr.asdict(self.device_info), + "services": [], + "api_version": attr.asdict(self.api_version), + } for comp_type, infos in self.info.items(): store_data[comp_type] = [attr.asdict(info) for info in infos.values()] diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index b73df42c8dc..e02958d5885 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -68,7 +68,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): @property def _supports_speed_levels(self) -> bool: - api_version = self._client.api_version + api_version = self._api_version return api_version.major == 1 and api_version.minor > 3 async def async_set_percentage(self, percentage: int) -> None: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e5992c84358..9182be8d496 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.9.0"], + "requirements": ["aioesphomeapi==3.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 7f8dba6c6e3..6cb0d7cf98e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.9.0 +aioesphomeapi==3.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6381470a6cd..4db30aecf05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.9.0 +aioesphomeapi==3.0.1 # homeassistant.components.flo aioflo==0.4.1 From 8255a2f6c8a136c74033abbe95651ab87075759a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 28 Jun 2021 13:49:58 +0200 Subject: [PATCH 593/750] Removal of stale add-on devices on startup (#52245) --- homeassistant/components/hassio/__init__.py | 22 ++++++++++++--------- homeassistant/components/hassio/const.py | 8 ++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1feb34cd173..6f983a04e79 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -47,6 +47,7 @@ from .const import ( ATTR_URL, ATTR_VERSION, DOMAIN, + SupervisorEntityModel, ) from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError, api_data @@ -597,7 +598,7 @@ def async_register_addons_in_dev_reg( params = { "config_entry_id": entry_id, "identifiers": {(DOMAIN, addon[ATTR_SLUG])}, - "model": "Home Assistant Add-on", + "model": SupervisorEntityModel.ADDON, "sw_version": addon[ATTR_VERSION], "name": addon[ATTR_NAME], "entry_type": ATTR_SERVICE, @@ -616,7 +617,7 @@ def async_register_os_in_dev_reg( "config_entry_id": entry_id, "identifiers": {(DOMAIN, "OS")}, "manufacturer": "Home Assistant", - "model": "Home Assistant Operating System", + "model": SupervisorEntityModel.OS, "sw_version": os_dict[ATTR_VERSION], "name": "Home Assistant Operating System", "entry_type": ATTR_SERVICE, @@ -625,9 +626,7 @@ def async_register_os_in_dev_reg( @callback -def async_remove_addons_from_dev_reg( - dev_reg: DeviceRegistry, addons: list[dict[str, Any]] -) -> None: +def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None: """Remove addons from the device registry.""" for addon_slug in addons: if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}): @@ -684,16 +683,21 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async_register_os_in_dev_reg( self.entry_id, self.dev_reg, new_data["os"] ) - return new_data # Remove add-ons that are no longer installed from device registry - if removed_addons := list(set(self.data["addons"]) - set(new_data["addons"])): - async_remove_addons_from_dev_reg(self.dev_reg, removed_addons) + supervisor_addon_devices = { + list(device.identifiers)[0][1] + for device in self.dev_reg.devices.values() + if self.entry_id in device.config_entries + and device.model == SupervisorEntityModel.ADDON + } + if stale_addons := supervisor_addon_devices - set(new_data["addons"]): + async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) # If there are new add-ons, we should reload the config entry so we can # create new devices and entities. We can return an empty dict because # coordinator will be recreated. - if list(set(new_data["addons"]) - set(self.data["addons"])): + if self.data and list(set(new_data["addons"]) - set(self.data["addons"])): self.hass.async_create_task( self.hass.config_entries.async_reload(self.entry_id) ) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 435d42349fd..6104e57fb17 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,4 +1,5 @@ """Hass.io const variables.""" +from enum import Enum DOMAIN = "hassio" @@ -46,3 +47,10 @@ ATTR_UPDATE_AVAILABLE = "update_available" ATTR_SLUG = "slug" ATTR_URL = "url" ATTR_REPOSITORY = "repository" + + +class SupervisorEntityModel(str, Enum): + """Supervisor entity model.""" + + ADDON = "Home Assistant Add-on" + OS = "Home Assistant Operating System" From 8133793f23dffd8beb23fd89155921488da7d5d6 Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 28 Jun 2021 13:57:01 +0200 Subject: [PATCH 594/750] Yamaha musiccast grouping-services (#51952) Co-authored-by: Tom Schneider --- .../components/yamaha_musiccast/__init__.py | 1 + .../components/yamaha_musiccast/const.py | 6 + .../yamaha_musiccast/media_player.py | 384 ++++++++++++++++++ 3 files changed, 391 insertions(+) diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index d3749da318c..3a8275e98f0 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -65,6 +65,7 @@ class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]): self.musiccast = client super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + self.entities: list[MusicCastDeviceEntity] = [] async def _async_update_data(self) -> MusicCastData: """Update data via library.""" diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 422f93e1562..b442a3135b9 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -1,4 +1,5 @@ """Constants for the MusicCast integration.""" + from homeassistant.components.media_player.const import ( REPEAT_MODE_ALL, REPEAT_MODE_OFF, @@ -16,6 +17,9 @@ ATTR_MODEL = "model" ATTR_PLAYLIST = "playlist" ATTR_PRESET = "preset" ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_MC_LINK = "mc_link" +ATTR_MAIN_SYNC = "main_sync" +ATTR_MC_LINK_SOURCES = [ATTR_MC_LINK, ATTR_MAIN_SYNC] DEFAULT_ZONE = "main" HA_REPEAT_MODE_TO_MC_MAPPING = { @@ -24,6 +28,8 @@ HA_REPEAT_MODE_TO_MC_MAPPING = { REPEAT_MODE_ALL: "all", } +NULL_GROUP = "00000000000000000000000000000000" + INTERVAL_SECONDS = "interval_seconds" MC_REPEAT_MODE_TO_HA_MAPPING = { diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index aab2c8df3d2..88033cf68d1 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -3,12 +3,14 @@ from __future__ import annotations import logging +from aiomusiccast import MusicCastGroupException import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( REPEAT_MODE_OFF, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -37,14 +39,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType, HomeAssistantType +from homeassistant.util import uuid from . import MusicCastDataUpdateCoordinator, MusicCastDeviceEntity from .const import ( + ATTR_MAIN_SYNC, + ATTR_MC_LINK, DEFAULT_ZONE, DOMAIN, HA_REPEAT_MODE_TO_MC_MAPPING, INTERVAL_SECONDS, MC_REPEAT_MODE_TO_HA_MAPPING, + NULL_GROUP, ) _LOGGER = logging.getLogger(__name__) @@ -64,6 +70,7 @@ MUSIC_PLAYER_SUPPORT = ( | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE | SUPPORT_STOP + | SUPPORT_GROUPING ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -146,12 +153,16 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self._cur_track = 0 self._repeat = REPEAT_MODE_OFF + self.coordinator.entities.append(self) async def async_added_to_hass(self): """Run when this Entity has been added to HA.""" await super().async_added_to_hass() # Sensors should also register callbacks to HA when their state changes self.coordinator.musiccast.register_callback(self.async_write_ha_state) + self.coordinator.musiccast.register_group_update_callback( + self.update_all_mc_entities + ) async def async_will_remove_from_hass(self): """Entity being removed from hass.""" @@ -164,6 +175,16 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """Push an update after each command.""" return False + @property + def ip_address(self): + """Return the ip address of the musiccast device.""" + return self.coordinator.musiccast.ip + + @property + def zone_id(self): + """Return the zone id of the musiccast device.""" + return self._zone_id + @property def _is_netusb(self): return ( @@ -288,6 +309,8 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @property def media_image_url(self): """Return the image url of current playing media.""" + if self.is_client and self.group_server != self: + return self.group_server.coordinator.musiccast.media_image_url return self.coordinator.musiccast.media_image_url if self._is_netusb else None @property @@ -408,3 +431,364 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return self.coordinator.data.netusb_play_time_updated return None + + # Group and MusicCast System specific functions/properties + + @property + def is_network_server(self) -> bool: + """Return only true if the current entity is a network server and not a main zone with an attached zone2.""" + return ( + self.coordinator.data.group_role == "server" + and self.coordinator.data.group_id != NULL_GROUP + and self._zone_id == self.coordinator.data.group_server_zone + ) + + @property + def other_zones(self) -> list[MusicCastMediaPlayer]: + """Return media player entities of the other zones of this device.""" + return [ + entity + for entity in self.coordinator.entities + if entity != self and isinstance(entity, MusicCastMediaPlayer) + ] + + @property + def is_server(self) -> bool: + """Return whether the media player is the server/host of the group. + + If the media player is not part of a group, False is returned. + """ + return self.is_network_server or ( + self._zone_id == DEFAULT_ZONE + and len( + [ + entity + for entity in self.other_zones + if entity.source == ATTR_MAIN_SYNC + ] + ) + > 0 + ) + + @property + def is_network_client(self) -> bool: + """Return True if the current entity is a network client and not just a main syncing entity.""" + return ( + self.coordinator.data.group_role == "client" + and self.coordinator.data.group_id != NULL_GROUP + and self.source == ATTR_MC_LINK + ) + + @property + def is_client(self) -> bool: + """Return whether the media player is the client of a group. + + If the media player is not part of a group, False is returned. + """ + return self.is_network_client or self.source == ATTR_MAIN_SYNC + + def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]: + """Return all media player entities of the musiccast system.""" + entities = [] + for coordinator in self.hass.data[DOMAIN].values(): + entities += [ + entity + for entity in coordinator.entities + if isinstance(entity, MusicCastMediaPlayer) + ] + return entities + + def get_all_server_entities(self) -> list[MusicCastMediaPlayer]: + """Return all media player entities in the musiccast system, which are in server mode.""" + entities = self.get_all_mc_entities() + return [entity for entity in entities if entity.is_server] + + def get_distribution_num(self) -> int: + """Return the distribution_num (number of clients in the whole musiccast system).""" + return sum( + [ + len(server.coordinator.data.group_client_list) + for server in self.get_all_server_entities() + ] + ) + + def is_part_of_group(self, group_server) -> bool: + """Return True if the given server is the server of self's group.""" + return group_server != self and ( + ( + self.ip_address in group_server.coordinator.data.group_client_list + and self.coordinator.data.group_id + == group_server.coordinator.data.group_id + and self.ip_address != group_server.ip_address + and self.source == ATTR_MC_LINK + ) + or ( + self.ip_address == group_server.ip_address + and self.source == ATTR_MAIN_SYNC + ) + ) + + @property + def group_server(self): + """Return the server of the own group if present, self else.""" + for entity in self.get_all_server_entities(): + if self.is_part_of_group(entity): + return entity + return self + + @property + def group_members(self) -> list[str] | None: + """Return a list of entity_ids, which belong to the group of self.""" + return [entity.entity_id for entity in self.musiccast_group] + + @property + def musiccast_group(self) -> list[MusicCastMediaPlayer]: + """Return all media players of the current group, if the media player is server.""" + if self.is_client: + # If we are a client we can still share group information, but we will take them from the server. + server = self.group_server + if server != self: + return server.musiccast_group + + return [self] + if not self.is_server: + return [self] + entities = self.get_all_mc_entities() + clients = [entity for entity in entities if entity.is_part_of_group(self)] + return [self] + clients + + @property + def musiccast_zone_entity(self) -> MusicCastMediaPlayer: + """Return the the entity of the zone, which is using MusicCast at the moment, if there is one, self else. + + It is possible that multiple zones use MusicCast as client at the same time. In this case the first one is + returned. + """ + for entity in self.other_zones: + if entity.is_network_server or entity.is_network_client: + return entity + + return self + + async def update_all_mc_entities(self): + """Update the whole musiccast system when group data change.""" + for entity in self.get_all_mc_entities(): + if entity.is_server: + await entity.async_check_client_list() + entity.async_write_ha_state() + + # Services + + async def async_join_players(self, group_members): + """Add all clients given in entities to the group of the server. + + Creates a new group if necessary. Used for join service. + """ + _LOGGER.info( + "%s wants to add the following entities %s", + self.entity_id, + str(group_members), + ) + + entities = [ + entity + for entity in self.get_all_mc_entities() + if entity.entity_id in group_members + ] + + if not self.is_server and self.musiccast_zone_entity.is_server: + # The MusicCast Distribution Module of this device is already in use. To use it as a server, we first + # have to unjoin and wait until the servers are updated. + await self.musiccast_zone_entity.async_server_close_group() + elif self.musiccast_zone_entity.is_client: + await self.async_client_leave_group(True) + # Use existing group id if we are server, generate a new one else. + group = ( + self.coordinator.data.group_id + if self.is_server + else uuid.random_uuid_hex().upper() + ) + # First let the clients join + for client in entities: + if client != self: + try: + await client.async_client_join(group, self) + except MusicCastGroupException: + _LOGGER.warning( + "%s is struggling to update its group data. Will retry perform the update", + client.entity_id, + ) + await client.async_client_join(group, self) + + await self.coordinator.musiccast.mc_server_group_extend( + self._zone_id, + [ + entity.ip_address + for entity in entities + if entity.ip_address != self.ip_address + ], + group, + self.get_distribution_num(), + ) + _LOGGER.debug( + "%s added the following entities %s", self.entity_id, str(entities) + ) + _LOGGER.info( + "%s has now the following musiccast group %s", + self.entity_id, + str(self.musiccast_group), + ) + + await self.update_all_mc_entities() + + async def async_unjoin_player(self): + """Leave the group. + + Stops the distribution if device is server. Used for unjoin service. + """ + _LOGGER.debug("%s called service unjoin", self.entity_id) + if self.is_server: + await self.async_server_close_group() + + else: + await self.async_client_leave_group() + + await self.update_all_mc_entities() + + # Internal client functions + + async def async_client_join(self, group_id, server): + """Let the client join a group. + + If this client is a server, the server will stop distributing. If the client is part of a different group, + it will leave that group first. + """ + # If we should join the group, which is served by the main zone, we can simply select main_sync as input. + _LOGGER.debug("%s called service client join", self.entity_id) + if self.state == STATE_OFF: + await self.async_turn_on() + if self.ip_address == server.ip_address: + if server.zone == DEFAULT_ZONE: + await self.async_select_source(ATTR_MAIN_SYNC) + server.async_write_ha_state() + return + + # It is not possible to join a group hosted by zone2 from main zone. + raise Exception("Can not join a zone other than main of the same device.") + + if self.musiccast_zone_entity.is_server: + # If one of the zones of the device is a server, we need to unjoin first. + _LOGGER.info( + "%s is a server of a group and has to stop distribution " + "to use MusicCast for %s", + self.musiccast_zone_entity.entity_id, + self.entity_id, + ) + await self.musiccast_zone_entity.async_server_close_group() + + elif self.is_client: + if self.coordinator.data.group_id == server.coordinator.data.group_id: + _LOGGER.warning("%s is already part of the group", self.entity_id) + return + + _LOGGER.info( + "%s is client in a different group, will unjoin first", + self.entity_id, + ) + await self.async_client_leave_group() + + elif ( + self.ip_address in server.coordinator.data.group_client_list + and self.coordinator.data.group_id == server.coordinator.data.group_id + and self.coordinator.data.group_role == "client" + ): + # The device is already part of this group (e.g. main zone is also a client of this group). + # Just select mc_link as source + await self.async_select_source(ATTR_MC_LINK) + # As the musiccast group has changed, we need to trigger the servers ha state. + # In other cases this happens due to the callback after the dist updated message. + server.async_write_ha_state() + return + + _LOGGER.debug("%s will now join as a client", self.entity_id) + await self.coordinator.musiccast.mc_client_join( + server.ip_address, group_id, self._zone_id + ) + + # Ensure that mc link is selected. If main sync was selected previously, it's possible that this does not + # happen automatically + await self.async_select_source(ATTR_MC_LINK) + + async def async_client_leave_group(self, force=False): + """Make self leave the group. + + Should only be called for clients. + """ + _LOGGER.debug("%s client leave called", self.entity_id) + if not force and ( + self.source == ATTR_MAIN_SYNC + or len( + [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK] + ) + > 0 + ): + # If we are only syncing to main or another zone is also using the musiccast module as client, don't + # kill the client session, just select a dummy source. + save_inputs = self.coordinator.musiccast.get_save_inputs(self._zone_id) + if len(save_inputs): + await self.async_select_source(save_inputs[0]) + # Then turn off the zone + await self.async_turn_off() + else: + servers = [ + server + for server in self.get_all_server_entities() + if server.coordinator.data.group_id == self.coordinator.data.group_id + ] + await self.coordinator.musiccast.mc_client_unjoin() + if len(servers): + await servers[0].coordinator.musiccast.mc_server_group_reduce( + servers[0].zone_id, [self.ip_address], self.get_distribution_num() + ) + + for server in self.get_all_server_entities(): + await server.async_check_client_list() + + # Internal server functions + + async def async_server_close_group(self): + """Close group of self. + + Should only be called for servers. + """ + _LOGGER.info("%s closes his group", self.entity_id) + for client in self.musiccast_group: + if client != self: + await client.async_client_leave_group() + await self.coordinator.musiccast.mc_server_group_close() + + async def async_check_client_list(self): + """Let the server check if all its clients are still part of his group.""" + _LOGGER.debug("%s updates his group members", self.entity_id) + client_ips_for_removal = [] + for expected_client_ip in self.coordinator.data.group_client_list: + if expected_client_ip not in [ + entity.ip_address for entity in self.musiccast_group + ]: + # The client is no longer part of the group. Prepare removal. + client_ips_for_removal.append(expected_client_ip) + + if len(client_ips_for_removal) > 0: + _LOGGER.info( + "%s says good bye to the following members %s", + self.entity_id, + str(client_ips_for_removal), + ) + await self.coordinator.musiccast.mc_server_group_reduce( + self._zone_id, client_ips_for_removal, self.get_distribution_num() + ) + if len(self.musiccast_group) < 2: + # The group is empty, stop distribution. + await self.async_server_close_group() + + self.async_write_ha_state() From 24ba81c3a2dbd06a7a33ce64eb859e8c2260a69c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 28 Jun 2021 14:01:29 +0200 Subject: [PATCH 595/750] Update new effect before calculating color on Philips TV (#52072) --- homeassistant/components/philips_js/light.py | 39 +++++++++++--------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 800604ad7c7..4c321468d79 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -237,29 +237,34 @@ class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): def _update_from_coordinator(self): current = self._tv.ambilight_current_configuration color = None - if current and current["isExpert"]: - if settings := _get_settings(current): - color = settings["color"] - - if color: - self._attr_hs_color = ( - color["hue"] * 360.0 / 255.0, - color["saturation"] * 100.0 / 255.0, - ) - self._attr_brightness = color["brightness"] - elif data := self._tv.ambilight_cached: - hsv_h, hsv_s, hsv_v = color_RGB_to_hsv(*_average_pixels(data)) - self._attr_hs_color = hsv_h, hsv_s - self._attr_brightness = hsv_v * 255.0 / 100.0 - else: - self._attr_hs_color = None - self._attr_brightness = None if (cache_keys := _get_cache_keys(self._tv)) != self._cache_keys: self._cache_keys = cache_keys self._attr_effect_list = self._calculate_effect_list() self._attr_effect = self._calculate_effect() + if current and current["isExpert"]: + if settings := _get_settings(current): + color = settings["color"] + + mode, _, _ = _parse_effect(self._attr_effect) + + if mode == EFFECT_EXPERT and color: + self._attr_hs_color = ( + color["hue"] * 360.0 / 255.0, + color["saturation"] * 100.0 / 255.0, + ) + self._attr_brightness = color["brightness"] + elif mode == EFFECT_MODE and self._tv.ambilight_cached: + hsv_h, hsv_s, hsv_v = color_RGB_to_hsv( + *_average_pixels(self._tv.ambilight_cached) + ) + self._attr_hs_color = hsv_h, hsv_s + self._attr_brightness = hsv_v * 255.0 / 100.0 + else: + self._attr_hs_color = None + self._attr_brightness = None + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" From 7a4f3fe7b85ffdd058f3456ed8fd711d709f4733 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Jun 2021 14:37:26 +0200 Subject: [PATCH 596/750] Filter MQTT light JSON attributes (#52242) --- .../components/mqtt/light/schema_basic.py | 25 +++++++++++++++++++ .../components/mqtt/light/schema_json.py | 4 ++- .../components/mqtt/light/schema_template.py | 3 +++ homeassistant/components/mqtt/mixins.py | 7 +++--- tests/components/mqtt/test_light.py | 11 ++++++++ tests/components/mqtt/test_light_json.py | 11 ++++++++ tests/components/mqtt/test_light_template.py | 11 ++++++++ 7 files changed, 68 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 214da1dd7bf..c4af68d3044 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -8,10 +8,14 @@ from homeassistant.components.light import ( ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_EFFECT_LIST, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, ATTR_WHITE_VALUE, ATTR_XY_COLOR, @@ -97,6 +101,25 @@ CONF_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" CONF_ON_COMMAND_TYPE = "on_command_type" +MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( + { + ATTR_COLOR_MODE, + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + } +) + DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = "MQTT LightEntity" DEFAULT_OPTIMISTIC = False @@ -205,6 +228,8 @@ async def async_setup_entity_basic( class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" + _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" self._brightness = None diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 0c8080db5d2..1223b79148a 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -61,7 +61,7 @@ from ... import mqtt from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA -from .schema_basic import CONF_BRIGHTNESS_SCALE +from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED _LOGGER = logging.getLogger(__name__) @@ -157,6 +157,8 @@ async def async_setup_entity_json( class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" + _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" self._state = False diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index d2df37828f1..9a23d9ea2f9 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -37,6 +37,7 @@ from ... import mqtt from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED _LOGGER = logging.getLogger(__name__) @@ -93,6 +94,8 @@ async def async_setup_entity_template( class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" + _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" self._state = False diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 46c4a88e8fe..b0c8b573b37 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -194,12 +194,13 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): class MqttAttributes(Entity): """Mixin used for platforms that support JSON attributes.""" - def __init__(self, config: dict, extra_blocked_attributes: list = None) -> None: + _attributes_extra_blocked = frozenset() + + def __init__(self, config: dict) -> None: """Initialize the JSON attributes mixin.""" self._attributes = None self._attributes_sub_state = None self._attributes_config = config - self._extra_blocked_attributes = extra_blocked_attributes or [] async def async_added_to_hass(self) -> None: """Subscribe MQTT events.""" @@ -230,7 +231,7 @@ class MqttAttributes(Entity): k: v for k, v in json_dict.items() if k not in MQTT_ATTRIBUTES_BLOCKED - and k not in self._extra_blocked_attributes + and k not in self._attributes_extra_blocked } self._attributes = filtered_dict self.async_write_ha_state() diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index efd3b1b2424..7341eeb67fc 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -161,6 +161,9 @@ import pytest from homeassistant import config as hass_config from homeassistant.components import light +from homeassistant.components.mqtt.light.schema_basic import ( + MQTT_LIGHT_ATTRIBUTES_BLOCKED, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -190,6 +193,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -2712,6 +2716,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f4bf11df026..8aba08f60d7 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -93,6 +93,9 @@ from unittest.mock import call, patch import pytest from homeassistant.components import light +from homeassistant.components.mqtt.light.schema_basic import ( + MQTT_LIGHT_ATTRIBUTES_BLOCKED, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -121,6 +124,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1714,6 +1718,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 2e726d40ef1..2dabf0b7e46 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -31,6 +31,9 @@ from unittest.mock import patch import pytest from homeassistant.components import light +from homeassistant.components.mqtt.light.schema_basic import ( + MQTT_LIGHT_ATTRIBUTES_BLOCKED, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -59,6 +62,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -870,6 +874,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 3027b848c1b956feb55b237d7866c104895cbafd Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 28 Jun 2021 15:01:31 +0200 Subject: [PATCH 597/750] Add reauth config flow to devolo Home Control (#49697) --- .../devolo_home_control/__init__.py | 4 +- .../devolo_home_control/config_flow.py | 67 ++++++++--- .../devolo_home_control/exceptions.py | 4 + .../devolo_home_control/strings.json | 6 +- .../devolo_home_control/translations/en.json | 6 +- .../devolo_home_control/test_config_flow.py | 105 +++++++++++++++++- 6 files changed, 170 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index ded30d75de9..4c8757e4eff 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import ( CONF_MYDEVOLO, @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) if not credentials_valid: - return False + raise ConfigEntryAuthFailed if await hass.async_add_executor_job(mydevolo.maintenance): raise ConfigEntryNotReady diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 4f605baf98d..10172b94452 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -8,7 +8,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import configure_mydevolo from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES -from .exceptions import CredentialsInvalid +from .exceptions import CredentialsInvalid, UuidChanged class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -22,13 +22,13 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } + self._reauth_entry = None + self._url = DEFAULT_MYDEVOLO async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" if self.show_advanced_options: - self.data_schema[ - vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO) - ] = str + self.data_schema[vol.Required(CONF_MYDEVOLO, default=self._url)] = str if user_input is None: return self._show_form(step_id="user") try: @@ -55,8 +55,36 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) + async def async_step_reauth(self, user_input): + """Handle reauthentication.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._url = user_input[CONF_MYDEVOLO] + self.data_schema = { + vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD): str, + } + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle a flow initiated by reauthentication.""" + if user_input is None: + return self._show_form(step_id="reauth_confirm") + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form( + step_id="reauth_confirm", errors={"base": "invalid_auth"} + ) + except UuidChanged: + return self._show_form( + step_id="reauth_confirm", errors={"base": "reauth_failed"} + ) + async def _connect_mydevolo(self, user_input): """Connect to mydevolo.""" + user_input[CONF_MYDEVOLO] = user_input.get(CONF_MYDEVOLO, self._url) mydevolo = configure_mydevolo(conf=user_input) credentials_valid = await self.hass.async_add_executor_job( mydevolo.credentials_valid @@ -64,17 +92,30 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not credentials_valid: raise CredentialsInvalid uuid = await self.hass.async_add_executor_job(mydevolo.uuid) - await self.async_set_unique_id(uuid) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title="devolo Home Control", - data={ - CONF_PASSWORD: mydevolo.password, - CONF_USERNAME: mydevolo.user, - CONF_MYDEVOLO: mydevolo.url, - }, + if not self._reauth_entry: + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="devolo Home Control", + data={ + CONF_PASSWORD: mydevolo.password, + CONF_USERNAME: mydevolo.user, + CONF_MYDEVOLO: mydevolo.url, + }, + ) + + if self._reauth_entry.unique_id != uuid: + # The old user and the new user are not the same. This could mess-up everything as all unique IDs might change. + raise UuidChanged + + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=uuid ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") @callback def _show_form(self, step_id, errors=None): diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py index 378efa41cc5..a89058e6c16 100644 --- a/homeassistant/components/devolo_home_control/exceptions.py +++ b/homeassistant/components/devolo_home_control/exceptions.py @@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError class CredentialsInvalid(HomeAssistantError): """Given credentials are invalid.""" + + +class UuidChanged(HomeAssistantError): + """UUID of the user changed.""" diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index cbc911fcd18..ba1bc20bfd2 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "reauth_failed": "Please use the same mydevolo user as before." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index d1b8645072f..e5ea6a49cd8 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "reauth_failed": "Please use the same mydevolo user as before." }, "step": { "user": { diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 94435545cc6..054b613f3a0 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -5,7 +5,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN -from homeassistant.config_entries import SOURCE_USER from .const import ( DISCOVERY_INFO, @@ -57,7 +56,7 @@ async def test_form_already_configured(hass): MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_USER}, + context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -70,7 +69,7 @@ async def test_form_advanced_options(hass): DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -157,6 +156,106 @@ async def test_zeroconf_wrong_device(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +async def test_form_reauth(hass): + """Test that the reauth confirmation form is served.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username-new", "password": "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.credentials_invalid +async def test_form_invalid_credentials_reauth(hass): + """Test if we get the error message on invalid credentials.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_uuid_change_reauth(hass): + """Test that the reauth confirmation form is served.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="789123", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username-new", "password": "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "reauth_failed"} + + async def _setup(hass, result): """Finish configuration steps.""" with patch( From 5e721b25662a6be28df6a03b76866baa97c86675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 28 Jun 2021 15:32:29 +0200 Subject: [PATCH 598/750] Update SMA device info on setup (#51159) * Update device info on setup * Remove migration --- homeassistant/components/sma/__init__.py | 4 ++++ homeassistant/components/sma/config_flow.py | 8 +++----- homeassistant/components/sma/const.py | 2 +- homeassistant/components/sma/sensor.py | 9 +++++++-- tests/components/sma/__init__.py | 2 -- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index d8fcbbf8099..5db3039af40 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -32,6 +32,7 @@ from .const import ( DOMAIN, PLATFORMS, PYSMA_COORDINATOR, + PYSMA_DEVICE_INFO, PYSMA_OBJECT, PYSMA_REMOVE_LISTENER, PYSMA_SENSORS, @@ -141,6 +142,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) + # Get updated device info + device_info = await sma.device_info() # Get all device sensors sensor_def = await sma.get_sensors() @@ -189,6 +192,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYSMA_COORDINATOR: coordinator, PYSMA_SENSORS: sensor_def, PYSMA_REMOVE_LISTENER: remove_stop_listener, + PYSMA_DEVICE_INFO: device_info, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index a5147098c9f..66d875562ab 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, DOMAIN, GROUPS +from .const import CONF_CUSTOM, CONF_GROUP, DOMAIN, GROUPS _LOGGER = logging.getLogger(__name__) @@ -63,7 +63,6 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: vol.UNDEFINED, CONF_SENSORS: [], CONF_CUSTOM: {}, - DEVICE_INFO: {}, } async def async_step_user( @@ -79,7 +78,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[CONF_PASSWORD] = user_input[CONF_PASSWORD] try: - self._data[DEVICE_INFO] = await validate_input(self.hass, user_input) + device_info = await validate_input(self.hass, user_input) except aiohttp.ClientError: errors["base"] = "cannot_connect" except InvalidAuth: @@ -91,7 +90,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(self._data[DEVICE_INFO]["serial"]) + await self.async_set_unique_id(device_info["serial"]) self._abort_if_unique_id_configured() return self.async_create_entry( title=self._data[CONF_HOST], data=self._data @@ -120,7 +119,6 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Import a config flow from configuration.""" device_info = await validate_input(self.hass, import_config) - import_config[DEVICE_INFO] = device_info # If unique is configured import was already run # This means remap was already done, so we can abort diff --git a/homeassistant/components/sma/const.py b/homeassistant/components/sma/const.py index 2e1086e48a2..91173d95493 100644 --- a/homeassistant/components/sma/const.py +++ b/homeassistant/components/sma/const.py @@ -6,6 +6,7 @@ PYSMA_COORDINATOR = "coordinator" PYSMA_OBJECT = "pysma" PYSMA_REMOVE_LISTENER = "remove_listener" PYSMA_SENSORS = "pysma_sensors" +PYSMA_DEVICE_INFO = "device_info" PLATFORMS = ["sensor"] @@ -14,7 +15,6 @@ CONF_FACTOR = "factor" CONF_GROUP = "group" CONF_KEY = "key" CONF_UNIT = "unit" -DEVICE_INFO = "device_info" DEFAULT_SCAN_INTERVAL = 5 diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 2ef999a6579..3894f864ffb 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -33,10 +33,10 @@ from .const import ( CONF_GROUP, CONF_KEY, CONF_UNIT, - DEVICE_INFO, DOMAIN, GROUPS, PYSMA_COORDINATOR, + PYSMA_DEVICE_INFO, PYSMA_SENSORS, ) @@ -124,6 +124,7 @@ async def async_setup_entry( coordinator = sma_data[PYSMA_COORDINATOR] used_sensors = sma_data[PYSMA_SENSORS] + device_info = sma_data[PYSMA_DEVICE_INFO] entities = [] for sensor in used_sensors: @@ -131,7 +132,7 @@ async def async_setup_entry( SMAsensor( coordinator, config_entry.unique_id, - config_entry.data[DEVICE_INFO], + device_info, sensor, ) ) @@ -185,11 +186,15 @@ class SMAsensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" + if not self._device_info: + return None + return { "identifiers": {(DOMAIN, self._config_entry_unique_id)}, "name": self._device_info["name"], "manufacturer": self._device_info["manufacturer"], "model": self._device_info["type"], + "sw_version": self._device_info["sw_version"], } @property diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 0797558958e..4210772420c 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -69,7 +69,6 @@ MOCK_CUSTOM_SENSOR2 = { MOCK_SETUP_DATA = dict( { "custom": {}, - "device_info": MOCK_DEVICE, "sensors": [], }, **MOCK_USER_INPUT, @@ -91,7 +90,6 @@ MOCK_CUSTOM_SETUP_DATA = dict( "unit": MOCK_CUSTOM_SENSOR2["unit"], }, }, - "device_info": MOCK_DEVICE, "sensors": [], }, **MOCK_USER_INPUT, From c86b563fe14699a627c74da302a9d00983f1e5b8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Jun 2021 15:49:25 +0200 Subject: [PATCH 599/750] Bump hatasmota to 0.2.19 (#52246) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 59b91b75903..f87e0c189f1 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.18"], + "requirements": ["hatasmota==0.2.19"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 6cb0d7cf98e..74df44077ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.18 +hatasmota==0.2.19 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4db30aecf05..0743c2a6ccf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ hangups==0.4.14 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.18 +hatasmota==0.2.19 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index e9aa291fe6d..fc1e7fd624b 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -342,7 +342,7 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota) state = hass.states.get("sensor.tasmota_energy_apparentpower_1") assert state.state == "9.0" state = hass.states.get("sensor.tasmota_energy_apparentpower_2") - assert state.state == STATE_UNKNOWN + assert state.state == "5.6" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"ApparentPower":2.3}}' @@ -350,9 +350,9 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota) state = hass.states.get("sensor.tasmota_energy_apparentpower_0") assert state.state == "2.3" state = hass.states.get("sensor.tasmota_energy_apparentpower_1") - assert state.state == STATE_UNKNOWN + assert state.state == "9.0" state = hass.states.get("sensor.tasmota_energy_apparentpower_2") - assert state.state == STATE_UNKNOWN + assert state.state == "5.6" # Test polled state update async_fire_mqtt_message( @@ -378,7 +378,7 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota) state = hass.states.get("sensor.tasmota_energy_apparentpower_1") assert state.state == "9.0" state = hass.states.get("sensor.tasmota_energy_apparentpower_2") - assert state.state == STATE_UNKNOWN + assert state.state == "5.6" async_fire_mqtt_message( hass, @@ -388,9 +388,9 @@ async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota) state = hass.states.get("sensor.tasmota_energy_apparentpower_0") assert state.state == "2.3" state = hass.states.get("sensor.tasmota_energy_apparentpower_1") - assert state.state == STATE_UNKNOWN + assert state.state == "9.0" state = hass.states.get("sensor.tasmota_energy_apparentpower_2") - assert state.state == STATE_UNKNOWN + assert state.state == "5.6" @pytest.mark.parametrize("status_sensor_disabled", [False]) From efee36a176bd61357282f11528f57043b068fc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 28 Jun 2021 15:54:23 +0200 Subject: [PATCH 600/750] Don't copy result to new list (#52248) --- homeassistant/components/hassio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6f983a04e79..6c71f2eb042 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -697,7 +697,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # If there are new add-ons, we should reload the config entry so we can # create new devices and entities. We can return an empty dict because # coordinator will be recreated. - if self.data and list(set(new_data["addons"]) - set(self.data["addons"])): + if self.data and set(new_data["addons"]) - set(self.data["addons"]): self.hass.async_create_task( self.hass.config_entries.async_reload(self.entry_id) ) From fd1d110b80bfd902683ce3648d82d4a67ba24e96 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 28 Jun 2021 15:38:12 +0100 Subject: [PATCH 601/750] Add config flow for Coinbase (#45354) Co-authored-by: Franck Nijhof --- .coveragerc | 2 +- CODEOWNERS | 1 + homeassistant/components/coinbase/__init__.py | 142 ++++---- .../components/coinbase/config_flow.py | 211 +++++++++++ homeassistant/components/coinbase/const.py | 290 +++++++++++++++ .../components/coinbase/manifest.json | 11 +- homeassistant/components/coinbase/sensor.py | 124 +++++-- .../components/coinbase/strings.json | 40 +++ .../components/coinbase/translations/en.json | 40 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/coinbase/__init__.py | 1 + tests/components/coinbase/common.py | 84 +++++ tests/components/coinbase/const.py | 33 ++ tests/components/coinbase/test_config_flow.py | 332 ++++++++++++++++++ tests/components/coinbase/test_init.py | 80 +++++ 16 files changed, 1299 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/coinbase/config_flow.py create mode 100644 homeassistant/components/coinbase/const.py create mode 100644 homeassistant/components/coinbase/strings.json create mode 100644 homeassistant/components/coinbase/translations/en.json create mode 100644 tests/components/coinbase/__init__.py create mode 100644 tests/components/coinbase/common.py create mode 100644 tests/components/coinbase/const.py create mode 100644 tests/components/coinbase/test_config_flow.py create mode 100644 tests/components/coinbase/test_init.py diff --git a/.coveragerc b/.coveragerc index 8ba4b81a0b8..4dc3df0ba41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -154,7 +154,7 @@ omit = homeassistant/components/clicksend_tts/notify.py homeassistant/components/cmus/media_player.py homeassistant/components/co2signal/* - homeassistant/components/coinbase/* + homeassistant/components/coinbase/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comfoconnect/fan.py homeassistant/components/concord232/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 5352cc8e675..f47cf29d6c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/climacell/* @raman325 homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington +homeassistant/components/coinbase/* @tombrien homeassistant/components/color_extractor/* @GenericStudent homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/compensation/* @Petro31 diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 5bcd330c9bb..0351ddf19d1 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -1,4 +1,6 @@ -"""Support for Coinbase.""" +"""The Coinbase integration.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -6,105 +8,121 @@ from coinbase.wallet.client import Client from coinbase.wallet.error import AuthenticationError import voluptuous as vol -from homeassistant.const import CONF_API_KEY +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle +from .const import ( + API_ACCOUNT_ID, + API_ACCOUNTS_DATA, + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + CONF_YAML_API_TOKEN, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "coinbase" - -CONF_API_SECRET = "api_secret" -CONF_ACCOUNT_CURRENCIES = "account_balance_currencies" -CONF_EXCHANGE_CURRENCIES = "exchange_rate_currencies" - +PLATFORMS = ["sensor"] MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -DATA_COINBASE = "coinbase_cache" CONFIG_SCHEMA = vol.Schema( + cv.deprecated(DOMAIN), { DOMAIN: vol.Schema( { vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_API_SECRET): cv.string, - vol.Optional(CONF_ACCOUNT_CURRENCIES): vol.All( + vol.Required(CONF_YAML_API_TOKEN): cv.string, + vol.Optional(CONF_CURRENCIES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCHANGE_RATES, default=[]): vol.All( cv.ensure_list, [cv.string] ), - vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } + }, ) }, extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): - """Set up the Coinbase component. - - Will automatically setup sensors to support - wallets discovered on the network. - """ - api_key = config[DOMAIN][CONF_API_KEY] - api_secret = config[DOMAIN][CONF_API_SECRET] - account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES) - exchange_currencies = config[DOMAIN][CONF_EXCHANGE_CURRENCIES] - - hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, api_secret) - - if not hasattr(coinbase_data, "accounts"): - return False - for account in coinbase_data.accounts: - if account_currencies is None or account.currency in account_currencies: - load_platform(hass, "sensor", DOMAIN, {"account": account}, config) - for currency in exchange_currencies: - if currency not in coinbase_data.exchange_rates.rates: - _LOGGER.warning("Currency %s not found", currency) - continue - native = coinbase_data.exchange_rates.currency - load_platform( - hass, - "sensor", +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Coinbase component.""" + if DOMAIN not in config: + return True + hass.async_create_task( + hass.config_entries.flow.async_init( DOMAIN, - {"native_currency": native, "exchange_currency": currency}, - config, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], ) + ) return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Coinbase from a config entry.""" + + client = await hass.async_add_executor_job( + Client, + entry.data[CONF_API_KEY], + entry.data[CONF_API_TOKEN], + ) + + hass.data.setdefault(DOMAIN, {}) + + hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( + CoinbaseData, client + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +def get_accounts(client): + """Handle paginated accounts.""" + response = client.get_accounts() + accounts = response[API_ACCOUNTS_DATA] + next_starting_after = response.pagination.next_starting_after + + while next_starting_after: + response = client.get_accounts(starting_after=next_starting_after) + accounts += response[API_ACCOUNTS_DATA] + next_starting_after = response.pagination.next_starting_after + + return accounts + + class CoinbaseData: """Get the latest data and update the states.""" - def __init__(self, api_key, api_secret): + def __init__(self, client): """Init the coinbase data object.""" - self.client = Client(api_key, api_secret) - self.update() + self.client = client + self.accounts = get_accounts(self.client) + self.exchange_rates = self.client.get_exchange_rates() + self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" try: - response = self.client.get_accounts() - accounts = response["data"] - - # Most of Coinbase's API seems paginated now (25 items per page, but first page has 24). - # This API gives a 'next_starting_after' property to send back as a 'starting_after' param. - # Their API documentation is not up to date when writing these lines (2021-05-20) - next_starting_after = response.pagination.next_starting_after - - while next_starting_after: - response = self.client.get_accounts(starting_after=next_starting_after) - accounts = accounts + response["data"] - next_starting_after = response.pagination.next_starting_after - - self.accounts = accounts - + self.accounts = get_accounts(self.client) self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: _LOGGER.error( diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py new file mode 100644 index 00000000000..24cbaaa32e0 --- /dev/null +++ b/homeassistant/components/coinbase/config_flow.py @@ -0,0 +1,211 @@ +"""Config flow for Coinbase integration.""" +import logging + +from coinbase.wallet.client import Client +from coinbase.wallet.error import AuthenticationError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import get_accounts +from .const import ( + API_ACCOUNT_CURRENCY, + API_RATES, + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + CONF_OPTIONS, + CONF_YAML_API_TOKEN, + DOMAIN, + RATES, + WALLETS, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_API_TOKEN): str, + } +) + + +async def validate_api(hass: core.HomeAssistant, data): + """Validate the credentials.""" + + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_API_KEY] == data[CONF_API_KEY]: + raise AlreadyConfigured + try: + client = await hass.async_add_executor_job( + Client, data[CONF_API_KEY], data[CONF_API_TOKEN] + ) + user = await hass.async_add_executor_job(client.get_current_user) + except AuthenticationError as error: + raise InvalidAuth from error + except ConnectionError as error: + raise CannotConnect from error + + return {"title": user["name"]} + + +async def validate_options( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, options +): + """Validate the requested resources are provided by API.""" + + client = hass.data[DOMAIN][config_entry.entry_id].client + + accounts = await hass.async_add_executor_job(get_accounts, client) + + accounts_currencies = [account[API_ACCOUNT_CURRENCY] for account in accounts] + available_rates = await hass.async_add_executor_job(client.get_exchange_rates) + if CONF_CURRENCIES in options: + for currency in options[CONF_CURRENCIES]: + if currency not in accounts_currencies: + raise CurrencyUnavaliable + + if CONF_EXCHANGE_RATES in options: + for rate in options[CONF_EXCHANGE_RATES]: + if rate not in available_rates[API_RATES]: + raise ExchangeRateUnavaliable + + return True + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Coinbase.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + options = {} + + if CONF_OPTIONS in user_input: + options = user_input.pop(CONF_OPTIONS) + + try: + info = await validate_api(self.hass, user_input) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=info["title"], data=user_input, options=options + ) + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, config): + """Handle import of Coinbase config from YAML.""" + cleaned_data = { + CONF_API_KEY: config[CONF_API_KEY], + CONF_API_TOKEN: config[CONF_YAML_API_TOKEN], + } + cleaned_data[CONF_OPTIONS] = { + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: [], + } + if CONF_CURRENCIES in config: + cleaned_data[CONF_OPTIONS][CONF_CURRENCIES] = config[CONF_CURRENCIES] + if CONF_EXCHANGE_RATES in config: + cleaned_data[CONF_OPTIONS][CONF_EXCHANGE_RATES] = config[ + CONF_EXCHANGE_RATES + ] + + return await self.async_step_user(user_input=cleaned_data) + + @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 Coinbase.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + + errors = {} + default_currencies = self.config_entry.options.get(CONF_CURRENCIES) + default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES) + + if user_input is not None: + # Pass back user selected options, even if bad + if CONF_CURRENCIES in user_input: + default_currencies = user_input[CONF_CURRENCIES] + + if CONF_EXCHANGE_RATES in user_input: + default_exchange_rates = user_input[CONF_EXCHANGE_RATES] + + try: + await validate_options(self.hass, self.config_entry, user_input) + except CurrencyUnavaliable: + errors["base"] = "currency_unavaliable" + except ExchangeRateUnavaliable: + errors["base"] = "exchange_rate_unavaliable" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_CURRENCIES, + default=default_currencies, + ): cv.multi_select(WALLETS), + vol.Optional( + CONF_EXCHANGE_RATES, + default=default_exchange_rates, + ): cv.multi_select(RATES), + } + ), + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate Coinbase API Key is already configured.""" + + +class CurrencyUnavaliable(exceptions.HomeAssistantError): + """Error to indicate the requested currency resource is not provided by the API.""" + + +class ExchangeRateUnavaliable(exceptions.HomeAssistantError): + """Error to indicate the requested exchange rate resource is not provided by the API.""" diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py new file mode 100644 index 00000000000..1f86c8026ec --- /dev/null +++ b/homeassistant/components/coinbase/const.py @@ -0,0 +1,290 @@ +"""Constants used for Coinbase.""" + +CONF_CURRENCIES = "account_balance_currencies" +CONF_EXCHANGE_RATES = "exchange_rate_currencies" +CONF_OPTIONS = "options" +DOMAIN = "coinbase" + +# These are constants used by the previous YAML configuration +CONF_YAML_API_TOKEN = "api_secret" + +# Constants for data returned by Coinbase API +API_ACCOUNT_AMOUNT = "amount" +API_ACCOUNT_BALANCE = "balance" +API_ACCOUNT_CURRENCY = "currency" +API_ACCOUNT_ID = "id" +API_ACCOUNT_NATIVE_BALANCE = "native_balance" +API_ACCOUNT_NAME = "name" +API_ACCOUNTS_DATA = "data" +API_RATES = "rates" + +WALLETS = { + "AAVE": "AAVE", + "ALGO": "ALGO", + "ATOM": "ATOM", + "BAL": "BAL", + "BAND": "BAND", + "BAT": "BAT", + "BCH": "BCH", + "BNT": "BNT", + "BSV": "BSV", + "BTC": "BTC", + "CGLD": "CLGD", + "CVC": "CVC", + "COMP": "COMP", + "DAI": "DAI", + "DNT": "DNT", + "EOS": "EOS", + "ETC": "ETC", + "ETH": "ETH", + "EUR": "EUR", + "FIL": "FIL", + "GBP": "GBP", + "GRT": "GRT", + "KNC": "KNC", + "LINK": "LINK", + "LRC": "LRC", + "LTC": "LTC", + "MANA": "MANA", + "MKR": "MKR", + "NMR": "NMR", + "NU": "NU", + "OMG": "OMG", + "OXT": "OXT", + "REP": "REP", + "REPV2": "REPV2", + "SNX": "SNX", + "UMA": "UMA", + "UNI": "UNI", + "USDC": "USDC", + "WBTC": "WBTC", + "XLM": "XLM", + "XRP": "XRP", + "XTZ": "XTZ", + "YFI": "YFI", + "ZRX": "ZRX", +} + +RATES = { + "1INCH": "1INCH", + "AAVE": "AAVE", + "ADA": "ADA", + "AED": "AED", + "AFN": "AFN", + "ALGO": "ALGO", + "ALL": "ALL", + "AMD": "AMD", + "ANG": "ANG", + "ANKR": "ANKR", + "AOA": "AOA", + "ARS": "ARS", + "ATOM": "ATOM", + "AUD": "AUD", + "AWG": "AWG", + "AZN": "AZN", + "BAL": "BAL", + "BAM": "BAM", + "BAND": "BAND", + "BAT": "BAT", + "BBD": "BBD", + "BCH": "BCH", + "BDT": "BDT", + "BGN": "BGN", + "BHD": "BHD", + "BIF": "BIF", + "BMD": "BMD", + "BND": "BND", + "BNT": "BNT", + "BOB": "BOB", + "BRL": "BRL", + "BSD": "BSD", + "BSV": "BSV", + "BTC": "BTC", + "BTN": "BTN", + "BWP": "BWP", + "BYN": "BYN", + "BYR": "BYR", + "BZD": "BZD", + "CAD": "CAD", + "CDF": "CDF", + "CGLD": "CGLD", + "CHF": "CHF", + "CLF": "CLF", + "CLP": "CLP", + "CNH": "CNH", + "CNY": "CNY", + "COMP": "COMP", + "COP": "COP", + "CRC": "CRC", + "CRV": "CRV", + "CUC": "CUC", + "CVC": "CVC", + "CVE": "CVE", + "CZK": "CZK", + "DAI": "DAI", + "DASH": "DASH", + "DJF": "DJF", + "DKK": "DKK", + "DNT": "DNT", + "DOP": "DOP", + "DZD": "DZD", + "EGP": "EGP", + "ENJ": "ENJ", + "EOS": "EOS", + "ERN": "ERN", + "ETB": "ETB", + "ETC": "ETC", + "ETH": "ETH", + "ETH2": "ETH2", + "EUR": "EUR", + "FIL": "FIL", + "FJD": "FJD", + "FKP": "FKP", + "FORTH": "FORTH", + "GBP": "GBP", + "GBX": "GBX", + "GEL": "GEL", + "GGP": "GGP", + "GHS": "GHS", + "GIP": "GIP", + "GMD": "GMD", + "GNF": "GNF", + "GRT": "GRT", + "GTQ": "GTQ", + "GYD": "GYD", + "HKD": "HKD", + "HNL": "HNL", + "HRK": "HRK", + "HTG": "HTG", + "HUF": "HUF", + "IDR": "IDR", + "ILS": "ILS", + "IMP": "IMP", + "INR": "INR", + "IQD": "IQD", + "ISK": "ISK", + "JEP": "JEP", + "JMD": "JMD", + "JOD": "JOD", + "JPY": "JPY", + "KES": "KES", + "KGS": "KGS", + "KHR": "KHR", + "KMF": "KMF", + "KNC": "KNC", + "KRW": "KRW", + "KWD": "KWD", + "KYD": "KYD", + "KZT": "KZT", + "LAK": "LAK", + "LBP": "LBP", + "LINK": "LINK", + "LKR": "LKR", + "LRC": "LRC", + "LRD": "LRD", + "LSL": "LSL", + "LTC": "LTC", + "LYD": "LYD", + "MAD": "MAD", + "MANA": "MANA", + "MATIC": "MATIC", + "MDL": "MDL", + "MGA": "MGA", + "MKD": "MKD", + "MKR": "MKR", + "MMK": "MMK", + "MNT": "MNT", + "MOP": "MOP", + "MRO": "MRO", + "MTL": "MTL", + "MUR": "MUR", + "MVR": "MVR", + "MWK": "MWK", + "MXN": "MXN", + "MYR": "MYR", + "MZN": "MZN", + "NAD": "NAD", + "NGN": "NGN", + "NIO": "NIO", + "NKN": "NKN", + "NMR": "NMR", + "NOK": "NOK", + "NPR": "NPR", + "NU": "NU", + "NZD": "NZD", + "OGN": "OGN", + "OMG": "OMG", + "OMR": "OMR", + "OXT": "OXT", + "PAB": "PAB", + "PEN": "PEN", + "PGK": "PGK", + "PHP": "PHP", + "PKR": "PKR", + "PLN": "PLN", + "PYG": "PYG", + "QAR": "QAR", + "REN": "REN", + "REP": "REP", + "RON": "RON", + "RSD": "RSD", + "RUB": "RUB", + "RWF": "RWF", + "SAR": "SAR", + "SBD": "SBD", + "SCR": "SCR", + "SEK": "SEK", + "SGD": "SGD", + "SHP": "SHP", + "SKL": "SKL", + "SLL": "SLL", + "SNX": "SNX", + "SOS": "SOS", + "SRD": "SRD", + "SSP": "SSP", + "STD": "STD", + "STORJ": "STORJ", + "SUSHI": "SUSHI", + "SVC": "SVC", + "SZL": "SZL", + "THB": "THB", + "TJS": "TJS", + "TMT": "TMT", + "TND": "TND", + "TOP": "TOP", + "TRY": "TRY", + "TTD": "TTD", + "TWD": "TWD", + "TZS": "TZS", + "UAH": "UAH", + "UGX": "UGX", + "UMA": "UMA", + "UNI": "UNI", + "USD": "USD", + "USDC": "USDC", + "UYU": "UYU", + "UZS": "UZS", + "VES": "VES", + "VND": "VND", + "VUV": "VUV", + "WBTC": "WBTC", + "WST": "WST", + "XAF": "XAF", + "XAG": "XAG", + "XAU": "XAU", + "XCD": "XCD", + "XDR": "XDR", + "XLM": "XLM", + "XOF": "XOF", + "XPD": "XPD", + "XPF": "XPF", + "XPT": "XPT", + "XTZ": "XTZ", + "YER": "YER", + "YFI": "YFI", + "ZAR": "ZAR", + "ZEC": "ZEC", + "ZMW": "ZMW", + "ZRX": "ZRX", + "ZWL": "ZWL", +} diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 4579aecdd5b..aa056409786 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -2,7 +2,12 @@ "domain": "coinbase", "name": "Coinbase", "documentation": "https://www.home-assistant.io/integrations/coinbase", - "requirements": ["coinbase==2.1.0"], - "codeowners": [], + "requirements": [ + "coinbase==2.1.0" + ], + "codeowners": [ + "@tombrien" + ], + "config_flow": true, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 3a0e689862f..b090add2ddd 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,7 +1,23 @@ """Support for Coinbase sensors.""" +import logging + from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION +from .const import ( + API_ACCOUNT_AMOUNT, + API_ACCOUNT_BALANCE, + API_ACCOUNT_CURRENCY, + API_ACCOUNT_ID, + API_ACCOUNT_NAME, + API_ACCOUNT_NATIVE_BALANCE, + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + ATTR_NATIVE_BALANCE = "Balance in native currency" CURRENCY_ICONS = { @@ -16,45 +32,79 @@ DEFAULT_COIN_ICON = "mdi:currency-usd-circle" ATTRIBUTION = "Data provided by coinbase.com" -DATA_COINBASE = "coinbase_cache" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Coinbase sensor platform.""" + instance = hass.data[DOMAIN][config_entry.entry_id] + hass.async_add_executor_job(instance.update) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Coinbase sensors.""" - if discovery_info is None: - return - if "account" in discovery_info: - account = discovery_info["account"] - sensor = AccountSensor( - hass.data[DATA_COINBASE], account["name"], account["balance"]["currency"] - ) - if "exchange_currency" in discovery_info: - sensor = ExchangeRateSensor( - hass.data[DATA_COINBASE], - discovery_info["exchange_currency"], - discovery_info["native_currency"], - ) + entities = [] - add_entities([sensor], True) + provided_currencies = [ + account[API_ACCOUNT_CURRENCY] for account in instance.accounts + ] + + if CONF_CURRENCIES in config_entry.options: + desired_currencies = config_entry.options[CONF_CURRENCIES] + else: + desired_currencies = provided_currencies + + exchange_native_currency = instance.exchange_rates.currency + + for currency in desired_currencies: + if currency not in provided_currencies: + _LOGGER.warning( + "The currency %s is no longer provided by your account, please check " + "your settings in Coinbase's developer tools", + currency, + ) + break + entities.append(AccountSensor(instance, currency)) + + if CONF_EXCHANGE_RATES in config_entry.options: + for rate in config_entry.options[CONF_EXCHANGE_RATES]: + entities.append( + ExchangeRateSensor( + instance, + rate, + exchange_native_currency, + ) + ) + + async_add_entities(entities) class AccountSensor(SensorEntity): """Representation of a Coinbase.com sensor.""" - def __init__(self, coinbase_data, name, currency): + def __init__(self, coinbase_data, currency): """Initialize the sensor.""" self._coinbase_data = coinbase_data - self._name = f"Coinbase {name}" - self._state = None - self._unit_of_measurement = currency - self._native_balance = None - self._native_currency = None + self._currency = currency + for account in coinbase_data.accounts: + if account.currency == currency: + self._name = f"Coinbase {account[API_ACCOUNT_NAME]}" + self._id = f"coinbase-{account[API_ACCOUNT_ID]}" + self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] + self._unit_of_measurement = account[API_ACCOUNT_CURRENCY] + self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ + API_ACCOUNT_AMOUNT + ] + self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][ + API_ACCOUNT_CURRENCY + ] + break @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the Unique ID of the sensor.""" + return self._id + @property def state(self): """Return the state of the sensor.""" @@ -82,10 +132,15 @@ class AccountSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: - if self._name == f"Coinbase {account['name']}": - self._state = account["balance"]["amount"] - self._native_balance = account["native_balance"]["amount"] - self._native_currency = account["native_balance"]["currency"] + if account.currency == self._currency: + self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] + self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ + API_ACCOUNT_AMOUNT + ] + self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][ + API_ACCOUNT_CURRENCY + ] + break class ExchangeRateSensor(SensorEntity): @@ -96,7 +151,10 @@ class ExchangeRateSensor(SensorEntity): self._coinbase_data = coinbase_data self.currency = exchange_currency self._name = f"{exchange_currency} Exchange Rate" - self._state = None + self._id = f"{coinbase_data.user_id}-xe-{exchange_currency}" + self._state = round( + 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2 + ) self._unit_of_measurement = native_currency @property @@ -104,6 +162,11 @@ class ExchangeRateSensor(SensorEntity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self._id + @property def state(self): """Return the state of the sensor.""" @@ -127,5 +190,6 @@ class ExchangeRateSensor(SensorEntity): def update(self): """Get the latest state of the sensor.""" self._coinbase_data.update() - rate = self._coinbase_data.exchange_rates.rates[self.currency] - self._state = round(1 / float(rate), 2) + self._state = round( + 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2 + ) diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json new file mode 100644 index 00000000000..5988f2a49a9 --- /dev/null +++ b/homeassistant/components/coinbase/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "title": "Coinbase API Key Details", + "description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "description": "Adjust Coinbase Options", + "data": { + "account_balance_currencies": "Wallet balances to report.", + "exchange_rate_currencies": "Exchange rates to report." + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.", + "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase." + } + } +} diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json new file mode 100644 index 00000000000..b72cd236fa4 --- /dev/null +++ b/homeassistant/components/coinbase/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" + }, + "description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")", + "title": "Coinbase API Key Details" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.", + "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Wallet balances to report.", + "exchange_rate_currencies": "Exchange rates to report." + }, + "description": "Adjust Coinbase Options" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 48545ff0e06..2f12ebd7d74 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ FLOWS = [ "cert_expiry", "climacell", "cloudflare", + "coinbase", "control4", "coolmaster", "coronavirus", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0743c2a6ccf..e1fc86963eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -249,6 +249,9 @@ buienradar==1.0.4 # homeassistant.components.caldav caldav==0.7.1 +# homeassistant.components.coinbase +coinbase==2.1.0 + # homeassistant.scripts.check_config colorlog==5.0.1 diff --git a/tests/components/coinbase/__init__.py b/tests/components/coinbase/__init__.py new file mode 100644 index 00000000000..d3629804954 --- /dev/null +++ b/tests/components/coinbase/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coinbase integration.""" diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py new file mode 100644 index 00000000000..5fcab6605bd --- /dev/null +++ b/tests/components/coinbase/common.py @@ -0,0 +1,84 @@ +"""Collection of helpers.""" +from homeassistant.components.coinbase.const import ( + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN + +from .const import GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2, MOCK_ACCOUNTS_RESPONSE + +from tests.common import MockConfigEntry + + +class MockPagination: + """Mock pagination result.""" + + def __init__(self, value=None): + """Load simple pagination for tests.""" + self.next_starting_after = value + + +class MockGetAccounts: + """Mock accounts with pagination.""" + + def __init__(self, starting_after=0): + """Init mocked object, forced to return two at a time.""" + if (target_end := starting_after + 2) >= ( + max_end := len(MOCK_ACCOUNTS_RESPONSE) + ): + end = max_end + self.pagination = MockPagination(value=None) + else: + end = target_end + self.pagination = MockPagination(value=target_end) + + self.accounts = { + "data": MOCK_ACCOUNTS_RESPONSE[starting_after:end], + } + self.started_at = starting_after + + def __getitem__(self, item): + """Handle subscript request.""" + return self.accounts[item] + + +def mocked_get_accounts(_, **kwargs): + """Return simplied accounts using mock.""" + return MockGetAccounts(**kwargs) + + +def mock_get_current_user(): + """Return a simplified mock user.""" + return { + "id": "123456-abcdef", + "name": "Test User", + } + + +def mock_get_exchange_rates(): + """Return a heavily reduced mock list of exchange rates for testing.""" + return { + "currency": "USD", + "rates": {GOOD_EXCHNAGE_RATE_2: "0.109", GOOD_EXCHNAGE_RATE: "0.00002"}, + } + + +async def init_mock_coinbase(hass): + """Init Coinbase integration for testing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="abcde12345", + title="Test User", + data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, + options={ + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: [], + }, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py new file mode 100644 index 00000000000..52505be514e --- /dev/null +++ b/tests/components/coinbase/const.py @@ -0,0 +1,33 @@ +"""Constants for testing the Coinbase integration.""" + +GOOD_CURRENCY = "BTC" +GOOD_CURRENCY_2 = "USD" +GOOD_CURRENCY_3 = "EUR" +GOOD_EXCHNAGE_RATE = "BTC" +GOOD_EXCHNAGE_RATE_2 = "ATOM" +BAD_CURRENCY = "ETH" +BAD_EXCHANGE_RATE = "ETH" + +MOCK_ACCOUNTS_RESPONSE = [ + { + "balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3}, + "currency": "BTC", + "id": "ABCDEF", + "name": "BTC Wallet", + "native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2}, + }, + { + "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY}, + "currency": "BTC", + "id": "123456789", + "name": "BTC Wallet", + "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, + }, + { + "balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, + "currency": "USD", + "id": "987654321", + "name": "USD Wallet", + "native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, + }, +] diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py new file mode 100644 index 00000000000..a03f423d852 --- /dev/null +++ b/tests/components/coinbase/test_config_flow.py @@ -0,0 +1,332 @@ +"""Test the Coinbase config flow.""" +from unittest.mock import patch + +from coinbase.wallet.error import AuthenticationError +from requests.models import Response + +from homeassistant import config_entries, setup +from homeassistant.components.coinbase.const import ( + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + CONF_YAML_API_TOKEN, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN + +from .common import ( + init_mock_coinbase, + mock_get_current_user, + mock_get_exchange_rates, + mocked_get_accounts, +) +from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), patch( + "homeassistant.components.coinbase.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.coinbase.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test User" + assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + response = Response() + response.status_code = 401 + api_auth_error = AuthenticationError( + response, + "authentication_error", + "invalid signature", + [{"id": "authentication_error", "message": "invalid signature"}], + ) + with patch( + "coinbase.wallet.client.Client.get_current_user", + side_effect=api_auth_error, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "coinbase.wallet.client.Client.get_current_user", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_catch_all_exception(hass): + """Test we handle unknown exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "coinbase.wallet.client.Client.get_current_user", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_option_good_account_currency(hass): + """Test we handle a good wallet currency option.""" + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [GOOD_CURRENCY], + CONF_EXCHANGE_RATES: [], + }, + ) + assert result2["type"] == "create_entry" + + +async def test_form_bad_account_currency(hass): + """Test we handle a bad currency option.""" + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [BAD_CURRENCY], + CONF_EXCHANGE_RATES: [], + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "currency_unavaliable"} + + +async def test_option_good_exchange_rate(hass): + """Test we handle a good exchange rate option.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="abcde12345", + title="Test User", + data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, + options={ + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: [], + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE], + }, + ) + assert result2["type"] == "create_entry" + + +async def test_form_bad_exchange_rate(hass): + """Test we handle a bad exchange rate.""" + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: [BAD_EXCHANGE_RATE], + }, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "exchange_rate_unavaliable"} + + +async def test_option_catch_all_exception(hass): + """Test we handle an unknown exception in the option flow.""" + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + config_entry = await init_mock_coinbase(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + + with patch( + "coinbase.wallet.client.Client.get_accounts", + side_effect=Exception, + ): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CURRENCIES: [], + CONF_EXCHANGE_RATES: ["ETH"], + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_yaml_import(hass): + """Test YAML import works.""" + conf = { + CONF_API_KEY: "123456", + CONF_YAML_API_TOKEN: "AbCDeF", + CONF_CURRENCIES: ["BTC", "USD"], + CONF_EXCHANGE_RATES: ["ATOM", "BTC"], + } + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + assert result["type"] == "create_entry" + assert result["title"] == "Test User" + assert result["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"} + assert result["options"] == { + CONF_CURRENCIES: ["BTC", "USD"], + CONF_EXCHANGE_RATES: ["ATOM", "BTC"], + } + + +async def test_yaml_existing(hass): + """Test YAML ignored when already processed.""" + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "123456", + CONF_API_TOKEN: "AbCDeF", + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "123456", + CONF_YAML_API_TOKEN: "AbCDeF", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py new file mode 100644 index 00000000000..612519b1cee --- /dev/null +++ b/tests/components/coinbase/test_init.py @@ -0,0 +1,80 @@ +"""Test the Coinbase integration.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.coinbase.const import ( + CONF_CURRENCIES, + CONF_EXCHANGE_RATES, + CONF_YAML_API_TOKEN, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.setup import async_setup_component + +from .common import ( + init_mock_coinbase, + mock_get_current_user, + mock_get_exchange_rates, + mocked_get_accounts, +) +from .const import ( + GOOD_CURRENCY, + GOOD_CURRENCY_2, + GOOD_EXCHNAGE_RATE, + GOOD_EXCHNAGE_RATE_2, +) + + +async def test_setup(hass): + """Test setting up from configuration.yaml.""" + conf = { + DOMAIN: { + CONF_API_KEY: "123456", + CONF_YAML_API_TOKEN: "AbCDeF", + CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + } + } + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", + new=mocked_get_accounts, + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + assert await async_setup_component(hass, DOMAIN, conf) + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].title == "Test User" + assert entries[0].source == config_entries.SOURCE_IMPORT + assert entries[0].options == { + CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2], + } + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + with patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), patch( + "coinbase.wallet.client.Client.get_accounts", + new=mocked_get_accounts, + ), patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ): + entry = await init_mock_coinbase(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == config_entries.ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) From 3a5ee000811fd54e8edb5cd08ba33b4c9d9c15c6 Mon Sep 17 00:00:00 2001 From: Xuefer Date: Mon, 28 Jun 2021 22:39:18 +0800 Subject: [PATCH 602/750] Merge onvif host/auth step, allow skipping scan (#49660) --- homeassistant/components/onvif/config_flow.py | 54 +++++++------- homeassistant/components/onvif/strings.json | 16 ++--- .../components/onvif/translations/en.json | 18 +++-- tests/components/onvif/test_config_flow.py | 72 +++++++++++-------- 4 files changed, 81 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 1fa904e67e4..dd90712d43e 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -91,10 +91,15 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle user flow.""" - if user_input is not None: - return await self.async_step_device() + if user_input: + if user_input["auto"]: + return await self.async_step_device() + return await self.async_step_configure() - return self.async_show_form(step_id="user") + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("auto", default=True): bool}), + ) async def async_step_device(self, user_input=None): """Handle WS-Discovery. @@ -105,7 +110,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input: if CONF_MANUAL_INPUT == user_input[CONF_HOST]: - return await self.async_step_manual_input() + return await self.async_step_configure() for device in self.devices: name = f"{device[CONF_NAME]} ({device[CONF_HOST]})" @@ -116,7 +121,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_HOST: device[CONF_HOST], CONF_PORT: device[CONF_PORT], } - return await self.async_step_auth() + return await self.async_step_configure() discovery = await async_discovery(self.hass) for device in discovery: @@ -142,44 +147,33 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(names)}), ) - return await self.async_step_manual_input() + return await self.async_step_configure() - async def async_step_manual_input(self, user_input=None): - """Manual configuration.""" + async def async_step_configure(self, user_input=None, errors=None): + """Device 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() + def conf(name, default=None): + return self.onvif_config.get(name, default) + # Username and Password are optional and default empty # due to some cameras not allowing you to change ONVIF user settings. # See https://github.com/home-assistant/core/issues/39182 # and https://github.com/home-assistant/core/issues/35904 return self.async_show_form( - step_id="auth", + step_id="configure", data_schema=vol.Schema( { - vol.Optional(CONF_USERNAME, default=""): str, - vol.Optional(CONF_PASSWORD, default=""): str, + vol.Required(CONF_NAME, default=conf(CONF_NAME)): str, + vol.Required(CONF_HOST, default=conf(CONF_HOST)): str, + vol.Required(CONF_PORT, default=conf(CONF_PORT, DEFAULT_PORT)): int, + vol.Optional(CONF_USERNAME, default=conf(CONF_USERNAME, "")): str, + vol.Optional(CONF_PASSWORD, default=conf(CONF_PASSWORD, "")): str, } ), + errors=errors, ) async def async_step_profiles(self, user_input=None): @@ -268,7 +262,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): finally: await device.close() - return self.async_show_form(step_id="auth", errors=errors) + return await self.async_step_configure(errors=errors) async def async_step_import(self, user_input): """Handle import.""" diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index dac8ef8647d..4cf1bd4bad0 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -12,6 +12,9 @@ }, "step": { "user": { + "data": { + "auto": "Search automatically" + }, "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." }, @@ -21,20 +24,15 @@ }, "title": "Select ONVIF device" }, - "manual_input": { + "configure": { "data": { "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]" - }, - "title": "Configure ONVIF device" - }, - "auth": { - "title": "Configure authentication", - "data": { + "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" - } + }, + "title": "Configure ONVIF device" }, "configure_profile": { "description": "Create camera entity for {profile} at {resolution} resolution?", diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index f52b96fbdc5..c3b328646ee 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -11,12 +11,15 @@ "cannot_connect": "Failed to connect" }, "step": { - "auth": { + "configure": { "data": { + "host": "Host", + "name": "Name", "password": "Password", + "port": "Port", "username": "Username" }, - "title": "Configure authentication" + "title": "Configure ONVIF device" }, "configure_profile": { "data": { @@ -31,15 +34,10 @@ }, "title": "Select ONVIF device" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Name", - "port": "Port" - }, - "title": "Configure ONVIF device" - }, "user": { + "data": { + "auto": "Search automatically" + }, "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" } diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 626eec433d1..e4cb079515c 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -203,7 +203,7 @@ async def test_flow_discovered_devices(hass): setup_mock_device(mock_device) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], user_input={"auto": True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -215,7 +215,7 @@ async def test_flow_discovered_devices(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" + assert result["step_id"] == "configure" with patch( "homeassistant.components.onvif.async_setup", return_value=True @@ -268,7 +268,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): setup_mock_device(mock_device) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], user_input={"auto": True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -281,7 +281,37 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "manual_input" + assert result["step_id"] == "configure" + + +async def test_flow_discovered_no_device(hass): + """Test that config flow discovery no device.""" + await setup_onvif_integration(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.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_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera) + mock_discovery.return_value = [] + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"auto": True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "configure" async def test_flow_discovery_ignore_existing_and_abort(hass): @@ -319,12 +349,12 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): setup_mock_device(mock_device) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], user_input={"auto": True} ) # 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" + assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -332,15 +362,6 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): 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, }, @@ -373,23 +394,11 @@ async def test_flow_manual_entry(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={}, + user_input={"auto": False}, ) 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" + assert result["step_id"] == "configure" with patch( "homeassistant.components.onvif.async_setup", return_value=True @@ -399,6 +408,9 @@ async def test_flow_manual_entry(hass): 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, config_flow.CONF_USERNAME: USERNAME, config_flow.CONF_PASSWORD: PASSWORD, }, @@ -598,7 +610,7 @@ async def test_flow_import_onvif_auth_error(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" + assert result["step_id"] == "configure" assert result["errors"]["base"] == "cannot_connect" From 540d6e9fa5f5eed5a34caed01d08b6dabda9d333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 28 Jun 2021 16:59:17 +0200 Subject: [PATCH 603/750] Use pysma exceptions (#52252) --- homeassistant/components/sma/__init__.py | 24 ++++++++++++++------ homeassistant/components/sma/config_flow.py | 25 +++++---------------- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sma/test_config_flow.py | 12 ++++++---- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 5db3039af40..0df3ef8cb7c 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -142,10 +142,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass, verify_ssl=verify_ssl) sma = pysma.SMA(session, url, password, group) - # Get updated device info - device_info = await sma.device_info() - # Get all device sensors - sensor_def = await sma.get_sensors() + try: + # Get updated device info + device_info = await sma.device_info() + # Get all device sensors + sensor_def = await sma.get_sensors() + except ( + pysma.exceptions.SmaReadException, + pysma.exceptions.SmaConnectionException, + ) as exc: + raise ConfigEntryNotReady from exc # Parse legacy options if initial setup was done from yaml if entry.source == SOURCE_IMPORT: @@ -155,9 +161,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Define the coordinator async def async_update_data(): """Update the used SMA sensors.""" - values = await sma.read(sensor_def) - if not values: - raise UpdateFailed + try: + await sma.read(sensor_def) + except ( + pysma.exceptions.SmaReadException, + pysma.exceptions.SmaConnectionException, + ) as exc: + raise UpdateFailed from exc interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 66d875562ab..b95e4e4fe06 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -4,11 +4,10 @@ from __future__ import annotations import logging from typing import Any -import aiohttp import pysma import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -36,15 +35,11 @@ async def validate_input( sma = pysma.SMA(session, url, data[CONF_PASSWORD], group=data[CONF_GROUP]) - if await sma.new_session() is False: - raise InvalidAuth - + # new_session raises SmaAuthenticationException on failure + await sma.new_session() device_info = await sma.device_info() await sma.close_session() - if not device_info: - raise CannotRetrieveDeviceInfo - return device_info @@ -79,11 +74,11 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: device_info = await validate_input(self.hass, user_input) - except aiohttp.ClientError: + except pysma.exceptions.SmaConnectionException: errors["base"] = "cannot_connect" - except InvalidAuth: + except pysma.exceptions.SmaAuthenticationException: errors["base"] = "invalid_auth" - except CannotRetrieveDeviceInfo: + except pysma.exceptions.SmaReadException: errors["base"] = "cannot_retrieve_device_info" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -128,11 +123,3 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=import_config[CONF_HOST], data=import_config ) - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" - - -class CannotRetrieveDeviceInfo(exceptions.HomeAssistantError): - """Error to indicate we cannot retrieve the device information.""" diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 66b845ac39f..721431b89a7 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.5.0"], + "requirements": ["pysma==0.6.0"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 74df44077ce..ee617b75b74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1747,7 +1747,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.5.0 +pysma==0.6.0 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1fc86963eb..3a46d378017 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.5.0 +pysma==0.6.0 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index dbcecbeb43c..f262f7eeba1 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -1,7 +1,11 @@ """Test the sma config flow.""" from unittest.mock import patch -import aiohttp +from pysma.exceptions import ( + SmaAuthenticationException, + SmaConnectionException, + SmaReadException, +) from homeassistant import setup from homeassistant.components.sma.const import DOMAIN @@ -54,7 +58,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "pysma.SMA.new_session", side_effect=aiohttp.ClientError + "pysma.SMA.new_session", side_effect=SmaConnectionException ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -73,7 +77,7 @@ async def test_form_invalid_auth(hass): ) with patch( - "pysma.SMA.new_session", return_value=False + "pysma.SMA.new_session", side_effect=SmaAuthenticationException ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -92,7 +96,7 @@ async def test_form_cannot_retrieve_device_info(hass): ) with patch("pysma.SMA.new_session", return_value=True), patch( - "pysma.SMA.read", return_value=False + "pysma.SMA.read", side_effect=SmaReadException ), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], From f2fe6c26aba9b6879f0e9099f3dbe652f35c9d3d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Mon, 28 Jun 2021 18:08:09 +0200 Subject: [PATCH 604/750] Add tests for LCN integration setup (#48070) --- .coveragerc | 3 - tests/components/lcn/conftest.py | 97 ++++++++++++++++++ tests/components/lcn/test_init.py | 106 ++++++++++++++++++++ tests/fixtures/lcn/config.json | 31 ++++++ tests/fixtures/lcn/config_entry_myhome.json | 11 ++ tests/fixtures/lcn/config_entry_pchk.json | 29 ++++++ 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 tests/components/lcn/conftest.py create mode 100644 tests/components/lcn/test_init.py create mode 100644 tests/fixtures/lcn/config.json create mode 100644 tests/fixtures/lcn/config_entry_myhome.json create mode 100644 tests/fixtures/lcn/config_entry_pchk.json diff --git a/.coveragerc b/.coveragerc index 4dc3df0ba41..64acaa2d8e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,15 +542,12 @@ omit = homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/const.py homeassistant/components/launch_library/sensor.py - homeassistant/components/lcn/__init__.py homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/climate.py - homeassistant/components/lcn/const.py homeassistant/components/lcn/cover.py homeassistant/components/lcn/helpers.py homeassistant/components/lcn/light.py homeassistant/components/lcn/scene.py - homeassistant/components/lcn/schemas.py homeassistant/components/lcn/sensor.py homeassistant/components/lcn/services.py homeassistant/components/lcn/switch.py diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py new file mode 100644 index 00000000000..aae4acfa914 --- /dev/null +++ b/tests/components/lcn/conftest.py @@ -0,0 +1,97 @@ +"""Test configuration and mocks for LCN component.""" +import json +from unittest.mock import AsyncMock, patch + +import pypck +from pypck.connection import PchkConnectionManager +import pypck.module +from pypck.module import GroupConnection, ModuleConnection +import pytest + +from homeassistant.components.lcn.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +class MockModuleConnection(ModuleConnection): + """Fake a LCN module connection.""" + + status_request_handler = AsyncMock() + activate_status_request_handler = AsyncMock() + cancel_status_request_handler = AsyncMock() + send_command = AsyncMock(return_value=True) + + +class MockGroupConnection(GroupConnection): + """Fake a LCN group connection.""" + + send_command = AsyncMock(return_value=True) + + +class MockPchkConnectionManager(PchkConnectionManager): + """Fake connection handler.""" + + async def async_connect(self, timeout=30): + """Mock establishing a connection to PCHK.""" + self.authentication_completed_future.set_result(True) + self.license_error_future.set_result(True) + self.segment_scan_completed_event.set() + + async def async_close(self): + """Mock closing a connection to PCHK.""" + + @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection) + @patch.object(pypck.connection, "GroupConnection", MockGroupConnection) + def get_address_conn(self, addr): + """Get LCN address connection.""" + return super().get_address_conn(addr, request_serials=False) + + send_command = AsyncMock() + + +def create_config_entry(name): + """Set up config entries with configuration data.""" + fixture_filename = f"lcn/config_entry_{name}.json" + entry_data = json.loads(load_fixture(fixture_filename)) + options = {} + + title = entry_data[CONF_HOST] + unique_id = fixture_filename + entry = MockConfigEntry( + domain=DOMAIN, + title=title, + unique_id=unique_id, + data=entry_data, + options=options, + ) + return entry + + +@pytest.fixture(name="entry") +def create_config_entry_pchk(): + """Return one specific config entry.""" + return create_config_entry("pchk") + + +@pytest.fixture(name="entry2") +def create_config_entry_myhome(): + """Return one specific config entry.""" + return create_config_entry("myhome") + + +async def init_integration(hass, entry): + """Set up the LCN integration in Home Assistant.""" + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def setup_component(hass): + """Set up the LCN component.""" + fixture_filename = "lcn/config.json" + config_data = json.loads(load_fixture(fixture_filename)) + + await async_setup_component(hass, DOMAIN, config_data) + await hass.async_block_till_done() diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py new file mode 100644 index 00000000000..eef02b681d8 --- /dev/null +++ b/tests/components/lcn/test_init.py @@ -0,0 +1,106 @@ +"""Test init of LCN integration.""" +from unittest.mock import patch + +from pypck.connection import ( + PchkAuthenticationError, + PchkConnectionManager, + PchkLicenseError, +) + +from homeassistant import config_entries +from homeassistant.components.lcn.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import MockPchkConnectionManager, init_integration, setup_component + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_entry(hass, entry): + """Test a successful setup entry and unload of entry.""" + await init_integration(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_multiple_entries(hass, entry, entry2): + """Test a successful setup and unload of multiple entries.""" + for config_entry in (entry, entry2): + await init_integration(hass, config_entry) + assert config_entry.state == ConfigEntryState.LOADED + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + for config_entry in (entry, entry2): + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert not hass.data.get(DOMAIN) + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_entry_update(hass, entry): + """Test a successful setup entry if entry with same id already exists.""" + # setup first entry + entry.source = config_entries.SOURCE_IMPORT + + # create dummy entity for LCN platform as an orphan + entity_registry = await er.async_get_registry(hass) + dummy_entity = entity_registry.async_get_or_create( + "switch", DOMAIN, "dummy", config_entry=entry + ) + assert dummy_entity in entity_registry.entities.values() + + # add entity to hass and setup (should cleanup dummy entity) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert dummy_entity not in entity_registry.entities.values() + + +async def test_async_setup_entry_raises_authentication_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object( + PchkConnectionManager, "async_connect", side_effect=PchkAuthenticationError + ): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_raises_license_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object( + PchkConnectionManager, "async_connect", side_effect=PchkLicenseError + ): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_raises_timeout_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object(PchkConnectionManager, "async_connect", side_effect=TimeoutError): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_from_configuration_yaml(hass): + """Test a successful setup using data from configuration.yaml.""" + await async_setup_component(hass, "persistent_notification", {}) + + with patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: + await setup_component(hass) + + assert async_setup_entry.await_count == 2 diff --git a/tests/fixtures/lcn/config.json b/tests/fixtures/lcn/config.json new file mode 100644 index 00000000000..50a1ca05e29 --- /dev/null +++ b/tests/fixtures/lcn/config.json @@ -0,0 +1,31 @@ +{ + "lcn": { + "connections": [ + { + "host": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "steps200", + "name": "pchk" + }, + { + "name": "myhome", + "host": "192.168.2.42", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "steps200" + } + ], + "switches": [ + { + "name": "Switch_Output1", + "address": "s0.m7", + "output": "output1" + } + ] + } +} diff --git a/tests/fixtures/lcn/config_entry_myhome.json b/tests/fixtures/lcn/config_entry_myhome.json new file mode 100644 index 00000000000..8ab59d0087d --- /dev/null +++ b/tests/fixtures/lcn/config_entry_myhome.json @@ -0,0 +1,11 @@ +{ + "host": "myhome", + "ip_address": "192.168.2.42", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "STEPS200", + "devices": [], + "entities": [] +} diff --git a/tests/fixtures/lcn/config_entry_pchk.json b/tests/fixtures/lcn/config_entry_pchk.json new file mode 100644 index 00000000000..3058389a95d --- /dev/null +++ b/tests/fixtures/lcn/config_entry_pchk.json @@ -0,0 +1,29 @@ +{ + "host": "pchk", + "ip_address": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "STEPS200", + "devices": [ + { + "address": [0, 7, false], + "name": "", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + } + ], + "entities": [ + { + "address": [0, 7, false], + "name": "Switch_Output1", + "resource": "output1", + "domain": "switch", + "domain_data": { + "output": "OUTPUT1" + } + } + ] +} From a1c741a46d7680f731a1dfe03826182e2b3ff8fe Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 28 Jun 2021 19:14:11 +0100 Subject: [PATCH 605/750] Provide correct defaults for CoinBase options flow (#52255) --- homeassistant/components/coinbase/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 24cbaaa32e0..fe0bff799c6 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -150,8 +150,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Manage the options.""" errors = {} - default_currencies = self.config_entry.options.get(CONF_CURRENCIES) - default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES) + default_currencies = self.config_entry.options.get(CONF_CURRENCIES, []) + default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES, []) if user_input is not None: # Pass back user selected options, even if bad From 6f41168616ca9433d53506a4b1b6a379650f23c0 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Mon, 28 Jun 2021 20:20:32 +0200 Subject: [PATCH 606/750] Change DiffuserRoomSize number entity to select entity (#51993) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../rituals_perfume_genie/__init__.py | 2 +- .../rituals_perfume_genie/number.py | 16 ++--- .../rituals_perfume_genie/select.py | 71 +++++++++++++++++++ 4 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/rituals_perfume_genie/select.py diff --git a/.coveragerc b/.coveragerc index 64acaa2d8e7..29ba3dcdeb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -847,6 +847,7 @@ omit = homeassistant/components/rituals_perfume_genie/binary_sensor.py homeassistant/components/rituals_perfume_genie/entity.py homeassistant/components/rituals_perfume_genie/number.py + homeassistant/components/rituals_perfume_genie/select.py homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 13699dcd64b..2bac2c03c04 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT -PLATFORMS = ["binary_sensor", "number", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "number", "select", "sensor", "switch"] EMPTY_CREDENTIALS = "" diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index 5fde2adcb6f..86274dc2f88 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -1,8 +1,6 @@ """Support for Rituals Perfume Genie numbers.""" from __future__ import annotations -import logging - from pyrituals import Diffuser from homeassistant.components.number import NumberEntity @@ -14,12 +12,8 @@ from . import RitualsDataUpdateCoordinator from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, SPEED from .entity import DiffuserEntity -_LOGGER = logging.getLogger(__name__) - MIN_PERFUME_AMOUNT = 1 MAX_PERFUME_AMOUNT = 3 -MIN_ROOM_SIZE = 1 -MAX_ROOM_SIZE = 4 PERFUME_AMOUNT_SUFFIX = " Perfume Amount" @@ -40,7 +34,7 @@ async def async_setup_entry( async_add_entities(entities) -class DiffuserPerfumeAmount(NumberEntity, DiffuserEntity): +class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity): """Representation of a diffuser perfume amount number.""" def __init__( @@ -74,9 +68,7 @@ class DiffuserPerfumeAmount(NumberEntity, DiffuserEntity): if value.is_integer() and MIN_PERFUME_AMOUNT <= value <= MAX_PERFUME_AMOUNT: await self._diffuser.set_perfume_amount(int(value)) else: - _LOGGER.warning( - "Can't set the perfume amount to %s. Perfume amount must be an integer between %s and %s, inclusive", - value, - MIN_PERFUME_AMOUNT, - MAX_PERFUME_AMOUNT, + raise ValueError( + f"Can't set the perfume amount to {value}. " + f"Perfume amount must be an integer between {self.min_value} and {self.max_value}, inclusive" ) diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py new file mode 100644 index 00000000000..17e71c38ae8 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/select.py @@ -0,0 +1,71 @@ +"""Support for Rituals Perfume Genie numbers.""" +from __future__ import annotations + +from pyrituals import Diffuser + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import AREA_SQUARE_METERS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RitualsDataUpdateCoordinator +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM +from .entity import DiffuserEntity + +ROOM_SIZE_SUFFIX = " Room Size" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the diffuser select entities.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + async_add_entities( + DiffuserRoomSize(diffuser, coordinators[hublot]) + for hublot, diffuser in diffusers.items() + ) + + +class DiffuserRoomSize(DiffuserEntity, SelectEntity): + """Representation of a diffuser room size select entity.""" + + _attr_icon = "mdi:ruler-square" + _attr_unit_of_measurement = AREA_SQUARE_METERS + _attr_options = ["15", "30", "60", "100"] + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the diffuser room size select entity.""" + super().__init__(diffuser, coordinator, ROOM_SIZE_SUFFIX) + self._attr_entity_registry_enabled_default = diffuser.has_battery + + @property + def current_option(self) -> str: + """Return the diffuser room size.""" + return { + "1": "15", + "2": "30", + "3": "60", + "4": "100", + }[self._diffuser.hub_data[ATTRIBUTES][ROOM]] + + async def async_select_option(self, option: str) -> None: + """Change the diffuser room size.""" + if option in self.options: + await self._diffuser.set_room_size( + { + "15": 1, + "30": 2, + "60": 3, + "100": 4, + }[option] + ) + else: + raise ValueError( + f"Can't set the room size to {option}. Allowed room sizes are: {self.options}" + ) From 9e50bd0b30f7429a4afaf727783e29ff55ce3f97 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 28 Jun 2021 19:21:04 +0100 Subject: [PATCH 607/750] Only load requested coinbase accounts (#51981) --- homeassistant/components/coinbase/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index b090add2ddd..b30fbabf8ef 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -44,10 +44,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): account[API_ACCOUNT_CURRENCY] for account in instance.accounts ] + desired_currencies = [] + if CONF_CURRENCIES in config_entry.options: desired_currencies = config_entry.options[CONF_CURRENCIES] - else: - desired_currencies = provided_currencies exchange_native_currency = instance.exchange_rates.currency From d4211c4a66ce1d0dd372327f4cc54ea5dc53a8d9 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 28 Jun 2021 20:22:44 +0200 Subject: [PATCH 608/750] Cleanup KNX supported_features for climate, cover and fan (#52218) --- homeassistant/components/knx/climate.py | 4 +++- homeassistant/components/knx/cover.py | 26 +++++++++++-------------- homeassistant/components/knx/fan.py | 8 +++----- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index aeef4a35c29..c2e2a269b27 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -172,12 +172,14 @@ class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" _device: XknxClimate - _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE _attr_temperature_unit = TEMP_CELSIUS def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" super().__init__(_create_climate(xknx, config)) + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + if self.preset_modes: + self._attr_supported_features |= SUPPORT_PRESET_MODE self._attr_target_temperature_step = self._device.temperature_step self._attr_unique_id = ( f"{self._device.temperature.group_address_state}_" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 92edf804bc6..5d32726474c 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -112,6 +112,17 @@ class KNXCover(KnxEntity, CoverEntity): self._attr_device_class = config.get(CONF_DEVICE_CLASS) or ( DEVICE_CLASS_BLIND if self._device.supports_angle else None ) + self._attr_supported_features = ( + SUPPORT_CLOSE | SUPPORT_OPEN | SUPPORT_SET_POSITION + ) + if self._device.supports_stop: + self._attr_supported_features |= SUPPORT_STOP | SUPPORT_STOP_TILT + if self._device.supports_angle: + self._attr_supported_features |= SUPPORT_SET_TILT_POSITION + if self._device.step.writable: + self._attr_supported_features |= ( + SUPPORT_CLOSE_TILT | SUPPORT_OPEN_TILT | SUPPORT_STOP_TILT + ) self._attr_unique_id = ( f"{self._device.updown.group_address}_" f"{self._device.position_target.group_address}" @@ -124,21 +135,6 @@ class KNXCover(KnxEntity, CoverEntity): if self._device.is_traveling(): self.start_auto_updater() - @property - def supported_features(self) -> int: - """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - if self._device.supports_stop: - supported_features |= SUPPORT_STOP - if self._device.supports_angle: - supported_features |= ( - SUPPORT_SET_TILT_POSITION - | SUPPORT_OPEN_TILT - | SUPPORT_CLOSE_TILT - | SUPPORT_STOP_TILT - ) - return supported_features - @property def current_cover_position(self) -> int | None: """Return the current position of the cover. diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 5d66eb1ceb6..f787795e1e8 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -66,11 +66,9 @@ class KNXFan(KnxEntity, FanEntity): # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None - self._attr_supported_features = ( - SUPPORT_SET_SPEED | SUPPORT_OSCILLATE - if self._device.supports_oscillation - else SUPPORT_SET_SPEED - ) + self._attr_supported_features = SUPPORT_SET_SPEED + if self._device.supports_oscillation: + self._attr_supported_features |= SUPPORT_OSCILLATE self._attr_unique_id = str(self._device.speed.group_address) async def async_set_percentage(self, percentage: int) -> None: From fbf85fd86b0f3ff43965c1179530670446098931 Mon Sep 17 00:00:00 2001 From: Farzad Noorian Date: Tue, 29 Jun 2021 04:43:23 +1000 Subject: [PATCH 609/750] Add OAuth 2.0 Bearer Token authentication to send_file for telegram_bot (#46567) --- .../components/telegram_bot/__init__.py | 5 +- .../components/telegram_bot/services.yaml | 132 ++++++++++++------ homeassistant/const.py | 1 + 3 files changed, 98 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 4b8e661b572..4231bcc46af 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -26,6 +26,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_PLATFORM, CONF_URL, + HTTP_BEARER_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import TemplateError @@ -255,7 +256,9 @@ def load_data( if url is not None: # Load data from URL params = {"timeout": 15} - if username is not None and password is not None: + if authentication == HTTP_BEARER_AUTHENTICATION and password is not None: + params["headers"] = {"Authorization": f"Bearer {password}"} + elif username is not None and password is not None: if authentication == HTTP_DIGEST_AUTHENTICATION: params["auth"] = HTTPDigestAuth(username, password) else: diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index dc3e9dde2d3..ea406cfdf96 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -29,9 +29,9 @@ send_message: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -65,7 +65,7 @@ send_message: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -94,16 +94,25 @@ send_photo: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -116,9 +125,9 @@ send_photo: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -151,7 +160,7 @@ send_photo: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -174,16 +183,25 @@ send_sticker: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -222,7 +240,7 @@ send_sticker: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -251,16 +269,25 @@ send_animation: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -273,9 +300,9 @@ send_animation: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -331,16 +358,25 @@ send_video: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -353,9 +389,9 @@ send_video: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -388,7 +424,7 @@ send_video: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -417,16 +453,25 @@ send_voice: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -465,7 +510,7 @@ send_voice: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -494,16 +539,25 @@ send_document: text: username: name: Username - description: Username for a URL which require HTTP basic authentication. + description: Username for a URL which require HTTP authentication. example: myuser selector: text: password: name: Password - description: Password for a URL which require HTTP basic authentication. + description: Password (or bearer token) for a URL which require HTTP authentication. example: myuser_pwd selector: text: + authentication: + name: Authentication method + description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`. + default: digest + selector: + select: + options: + - "digest" + - "bearer_token" target: name: Target description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. @@ -516,9 +570,9 @@ send_document: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_notification: name: Disable notification description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -551,7 +605,7 @@ send_document: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -569,7 +623,7 @@ send_location: min: -90 max: 90 step: 0.001 - unit_of_measurement: '°' + unit_of_measurement: "°" longitude: name: Longitude description: The longitude to send. @@ -579,7 +633,7 @@ send_location: min: -180 max: 180 step: 0.001 - unit_of_measurement: '°' + unit_of_measurement: "°" target: name: Target description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. @@ -613,7 +667,7 @@ send_location: object: message_tag: name: Message tag - description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' + description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}" example: "msg_to_edit" selector: text: @@ -654,9 +708,9 @@ edit_message: selector: select: options: - - 'html' - - 'markdown' - - 'markdown2' + - "html" + - "markdown" + - "markdown2" disable_web_page_preview: name: Disable web page preview description: Disables link previews for links in the message. diff --git a/homeassistant/const.py b/homeassistant/const.py index f5308148823..250a50bfada 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -637,6 +637,7 @@ HTTP_BAD_GATEWAY: Final = 502 HTTP_SERVICE_UNAVAILABLE: Final = 503 HTTP_BASIC_AUTHENTICATION: Final = "basic" +HTTP_BEARER_AUTHENTICATION: Final = "bearer_token" HTTP_DIGEST_AUTHENTICATION: Final = "digest" HTTP_HEADER_X_REQUESTED_WITH: Final = "X-Requested-With" From f538e07902b5370fdf448627798444df43a32085 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 28 Jun 2021 14:36:18 -0500 Subject: [PATCH 610/750] Update Tile unique ID to include username (#52175) --- homeassistant/components/tile/__init__.py | 27 ++++++++++++++++++- .../components/tile/device_tracker.py | 9 ++++--- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 91e1567cd65..2f00dabbcb6 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -7,7 +7,7 @@ from pytile.errors import InvalidAuthError, SessionExpiredError, TileError from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, entity_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency @@ -33,6 +33,31 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} + # The existence of shared Tiles across multiple accounts requires an entity ID + # change: + # + # Old: tile_{uuid} + # New: {username}_{uuid} + # + # Find any entities with the old format and update them: + ent_reg = entity_registry.async_get(hass) + for entity in [ + e + for e in ent_reg.entities.values() + if e.config_entry_id == entry.entry_id + and not e.unique_id.startswith(entry.data[CONF_USERNAME]) + ]: + new_unique_id = f"{entry.data[CONF_USERNAME]}_".join( + entity.unique_id.split(f"{DOMAIN}_") + ) + LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity.entity_id, + entity.unique_id, + new_unique_id, + ) + ent_reg.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) + websession = aiohttp_client.async_get_clientsession(hass) try: diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 7571e235ef1..add6e5f94a0 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -30,7 +30,9 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities( [ TileDeviceTracker( - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], tile + entry, + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], + tile, ) for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items() ] @@ -61,10 +63,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Representation of a network infrastructure device.""" - def __init__(self, coordinator, tile): + def __init__(self, entry, coordinator, tile): """Initialize.""" super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._entry = entry self._tile = tile @property @@ -116,7 +119,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): @property def unique_id(self): """Return the unique ID of the entity.""" - return f"tile_{self._tile.uuid}" + return f"{self._entry.data[CONF_USERNAME]}_{self._tile.uuid}" @property def source_type(self): diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index a17c099509e..e8d386f4a88 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.0"], + "requirements": ["pytile==5.2.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ee617b75b74..8e422d67a16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1918,7 +1918,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.0 +pytile==5.2.2 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a46d378017..22efc23a38b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ python-velbus==2.1.2 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.0 +pytile==5.2.2 # homeassistant.components.traccar pytraccar==0.9.0 From c6efdedd3c10f5ba41dcaf97ac754ca23f13482e Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 28 Jun 2021 22:33:15 +0200 Subject: [PATCH 611/750] Add AsusWRT load average sensors (#52230) --- homeassistant/components/asuswrt/const.py | 1 + homeassistant/components/asuswrt/router.py | 16 +++++++++++++++- homeassistant/components/asuswrt/sensor.py | 13 +++++++++++++ tests/components/asuswrt/test_sensor.py | 10 ++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index b450030ea3a..e41d683a7df 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -23,4 +23,5 @@ PROTOCOL_TELNET = "telnet" # Sensors SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"] SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"] +SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"] SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index b94de51b2fb..3c911d7712e 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -42,6 +42,7 @@ from .const import ( PROTOCOL_TELNET, SENSORS_BYTES, SENSORS_CONNECTED_DEVICE, + SENSORS_LOAD_AVG, SENSORS_RATES, ) @@ -54,6 +55,7 @@ SCAN_INTERVAL = timedelta(seconds=30) SENSORS_TYPE_BYTES = "sensors_bytes" SENSORS_TYPE_COUNT = "sensors_count" +SENSORS_TYPE_LOAD_AVG = "sensors_load_avg" SENSORS_TYPE_RATES = "sensors_rates" _LOGGER = logging.getLogger(__name__) @@ -100,6 +102,15 @@ class AsusWrtSensorDataHandler: return _get_dict(SENSORS_RATES, rates) + async def _get_load_avg(self): + """Fetch load average information from the router.""" + try: + avg = await self._api.async_get_loadavg() + except (OSError, ValueError) as exc: + raise UpdateFailed(exc) from exc + + return _get_dict(SENSORS_LOAD_AVG, avg) + def update_device_count(self, conn_devices: int): """Update connected devices attribute.""" if self._connected_devices == conn_devices: @@ -113,6 +124,8 @@ class AsusWrtSensorDataHandler: method = self._get_connected_devices elif sensor_type == SENSORS_TYPE_BYTES: method = self._get_bytes + elif sensor_type == SENSORS_TYPE_LOAD_AVG: + method = self._get_load_avg elif sensor_type == SENSORS_TYPE_RATES: method = self._get_rates else: @@ -316,8 +329,9 @@ class AsusWrtRouter: self._sensors_data_handler.update_device_count(self._connected_devices) sensors_types = { - SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, SENSORS_TYPE_BYTES: SENSORS_BYTES, + SENSORS_TYPE_COUNT: SENSORS_CONNECTED_DEVICE, + SENSORS_TYPE_LOAD_AVG: SENSORS_LOAD_AVG, SENSORS_TYPE_RATES: SENSORS_RATES, } diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index cfa8748d2ba..086c7373a4e 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -20,6 +20,7 @@ from .const import ( DOMAIN, SENSORS_BYTES, SENSORS_CONNECTED_DEVICE, + SENSORS_LOAD_AVG, SENSORS_RATES, ) from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter @@ -67,6 +68,18 @@ CONNECTION_SENSORS = { SENSOR_FACTOR: 1000000000, SENSOR_ICON: "mdi:upload", }, + SENSORS_LOAD_AVG[0]: { + SENSOR_NAME: "Load Avg (1m)", + SENSOR_ICON: "mdi:cpu-32-bit", + }, + SENSORS_LOAD_AVG[1]: { + SENSOR_NAME: "Load Avg (5m)", + SENSOR_ICON: "mdi:cpu-32-bit", + }, + SENSORS_LOAD_AVG[2]: { + SENSOR_NAME: "Load Avg (15m)", + SENSOR_ICON: "mdi:cpu-32-bit", + }, } _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index cbdded64ff9..19c27777c2a 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -39,6 +39,7 @@ CONFIG_DATA = { MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] +MOCK_LOAD_AVG = [1.1, 1.2, 1.3] SENSOR_NAMES = [ "Devices Connected", @@ -46,6 +47,9 @@ SENSOR_NAMES = [ "Download", "Upload Speed", "Upload", + "Load Avg (1m)", + "Load Avg (5m)", + "Load Avg (15m)", ] @@ -81,6 +85,9 @@ def mock_controller_connect(mock_devices): service_mock.return_value.async_get_current_transfer_rates = AsyncMock( return_value=MOCK_CURRENT_TRANSFER_RATES ) + service_mock.return_value.async_get_loadavg = AsyncMock( + return_value=MOCK_LOAD_AVG + ) yield service_mock @@ -125,6 +132,9 @@ async def test_sensors(hass, connect, mock_devices): assert hass.states.get(f"{sensor_prefix}_download").state == "60.0" assert hass.states.get(f"{sensor_prefix}_upload_speed").state == "80.0" assert hass.states.get(f"{sensor_prefix}_upload").state == "50.0" + assert hass.states.get(f"{sensor_prefix}_load_avg_1m").state == "1.1" + assert hass.states.get(f"{sensor_prefix}_load_avg_5m").state == "1.2" + assert hass.states.get(f"{sensor_prefix}_load_avg_15m").state == "1.3" assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2" # add one device and remove another From 42c944ce5650a8133d02474601c37af15a6e520d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 28 Jun 2021 22:48:29 +0100 Subject: [PATCH 612/750] Add secondary temperature sensors to homekit_controller (#52194) --- .../components/homekit_controller/const.py | 2 ++ .../components/homekit_controller/sensor.py | 31 +++++++++++++++---- .../specific_devices/test_ecobee3.py | 3 ++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index a3f7a9b7921..9fbc8fc4c62 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -44,5 +44,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { } CHARACTERISTIC_PLATFORMS = { + CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", + CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 0aa8d24da38..fe98b75130c 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,8 +1,8 @@ """Support for Homekit sensors.""" -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, @@ -28,14 +28,23 @@ SIMPLE_SENSOR = { CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, + "state_class": STATE_CLASS_MEASUREMENT, "unit": "watts", - "icon": "mdi:chart-line", }, CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { "name": "Real Time Energy", "device_class": DEVICE_CLASS_POWER, + "state_class": STATE_CLASS_MEASUREMENT, "unit": "watts", - "icon": "mdi:chart-line", + }, + CharacteristicsTypes.get_uuid(CharacteristicsTypes.TEMPERATURE_CURRENT): { + "name": "Current Temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "state_class": STATE_CLASS_MEASUREMENT, + "unit": TEMP_CELSIUS, + # This sensor is only for temperature characteristics that are not part + # of a temperature sensor service. + "probe": lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR, }, } @@ -216,12 +225,15 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): info, char, device_class=None, + state_class=None, unit=None, icon=None, name=None, + **kwargs, ): """Initialise a secondary HomeKit characteristic sensor.""" self._device_class = device_class + self._state_class = state_class self._unit = unit self._icon = icon self._name = name @@ -235,9 +247,14 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def device_class(self): - """Return units for the sensor.""" + """Return type of sensor.""" return self._device_class + @property + def state_class(self): + """Return type of state.""" + return self._state_class + @property def unit_of_measurement(self): """Return units for the sensor.""" @@ -285,10 +302,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): conn.add_listener(async_add_service) @callback - def async_add_characteristic(char): + def async_add_characteristic(char: Characteristic): kwargs = SIMPLE_SENSOR.get(char.type) if not kwargs: return False + if "probe" in kwargs and not kwargs["probe"](char): + return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index cc6c7ae4b9f..96dbd3b0718 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -59,6 +59,9 @@ async def test_ecobee3_setup(hass): assert climate_state.attributes["min_humidity"] == 20 assert climate_state.attributes["max_humidity"] == 50 + climate_sensor = entity_registry.async_get("sensor.homew_current_temperature") + assert climate_sensor.unique_id == "homekit-123456789012-aid:1-sid:16-cid:16" + occ1 = entity_registry.async_get("binary_sensor.kitchen") assert occ1.unique_id == "homekit-AB1C-56" From 7ce4763784ea157792bf2131e1fca5f13b3a9ba8 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 29 Jun 2021 00:39:21 +0200 Subject: [PATCH 613/750] change processor_temperature icon (#52256) * change processor_temperature icon to indicate the temperature, using "mdi:thermometer" * add DEVICE_CLASS_TEMPERATURE, * add None for icon * remove Icon from Last boot since it uses the device_class already --- homeassistant/components/systemmonitor/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 6e23faf606c..a218e627eb6 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( DATA_GIBIBYTES, DATA_MEBIBYTES, DATA_RATE_MEGABYTES_PER_SECOND, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, @@ -72,7 +73,7 @@ SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] = ), "ipv4_address": ("IPv4 address", "", "mdi:server-network", None, True), "ipv6_address": ("IPv6 address", "", "mdi:server-network", None, True), - "last_boot": ("Last boot", None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False), + "last_boot": ("Last boot", None, None, DEVICE_CLASS_TIMESTAMP, False), "load_15m": ("Load (15m)", " ", CPU_ICON, None, False), "load_1m": ("Load (1m)", " ", CPU_ICON, None, False), "load_5m": ("Load (5m)", " ", CPU_ICON, None, False), @@ -108,8 +109,8 @@ SENSOR_TYPES: dict[str, tuple[str, str | None, str | None, str | None, bool]] = "processor_temperature": ( "Processor temperature", TEMP_CELSIUS, - CPU_ICON, None, + DEVICE_CLASS_TEMPERATURE, False, ), "swap_free": ("Swap free", DATA_MEBIBYTES, "mdi:harddisk", None, False), From dc94a45832ca38646132d3c40b856066d0e70362 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 29 Jun 2021 00:44:13 +0200 Subject: [PATCH 614/750] Clean up Rituals Perfume Genie integration (#52266) --- .../rituals_perfume_genie/__init__.py | 4 +--- .../rituals_perfume_genie/binary_sensor.py | 1 - .../components/rituals_perfume_genie/const.py | 4 ---- .../components/rituals_perfume_genie/entity.py | 3 ++- .../components/rituals_perfume_genie/number.py | 4 ++-- .../components/rituals_perfume_genie/select.py | 18 +++--------------- .../components/rituals_perfume_genie/sensor.py | 12 ++++-------- .../components/rituals_perfume_genie/switch.py | 6 +++--- 8 files changed, 15 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 2bac2c03c04..114c7f6d400 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -15,8 +15,6 @@ from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT PLATFORMS = ["binary_sensor", "number", "select", "sensor", "switch"] -EMPTY_CREDENTIALS = "" - _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL = timedelta(seconds=30) @@ -25,7 +23,7 @@ UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Rituals Perfume Genie from a config entry.""" session = async_get_clientsession(hass) - account = Account(EMPTY_CREDENTIALS, EMPTY_CREDENTIALS, session) + account = Account(session=session) account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} try: diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index 2d82982388d..8446d899480 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -16,7 +16,6 @@ from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity CHARGING_SUFFIX = " Battery Charging" -BATTERY_CHARGING_ID = 21 async def async_setup_entry( diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 5ba687c3d7c..bafdef9140c 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -5,9 +5,5 @@ COORDINATORS = "coordinators" DEVICES = "devices" ACCOUNT_HASH = "account_hash" -ATTRIBUTES = "attributes" HUBLOT = "hublot" -ID = "id" -ROOM = "roomc" SENSORS = "sensors" -SPEED = "speedc" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 1c1f3912c68..d03450609e7 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -7,12 +7,13 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, DOMAIN, HUBLOT, SENSORS +from .const import DOMAIN, HUBLOT, SENSORS MANUFACTURER = "Rituals Cosmetics" MODEL = "The Perfume Genie" MODEL2 = "The Perfume Genie 2.0" +ATTRIBUTES = "attributes" ROOMNAME = "roomnamec" STATUS = "status" VERSION = "versionc" diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index 86274dc2f88..bc9ac7c9f31 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, SPEED +from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity MIN_PERFUME_AMOUNT = 1 @@ -51,7 +51,7 @@ class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity): @property def value(self) -> int: """Return the current perfume amount.""" - return self._diffuser.hub_data[ATTRIBUTES][SPEED] + return self._diffuser.perfume_amount @property def min_value(self) -> int: diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py index 17e71c38ae8..ac6f4aa872a 100644 --- a/homeassistant/components/rituals_perfume_genie/select.py +++ b/homeassistant/components/rituals_perfume_genie/select.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM +from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity ROOM_SIZE_SUFFIX = " Room Size" @@ -47,24 +47,12 @@ class DiffuserRoomSize(DiffuserEntity, SelectEntity): @property def current_option(self) -> str: """Return the diffuser room size.""" - return { - "1": "15", - "2": "30", - "3": "60", - "4": "100", - }[self._diffuser.hub_data[ATTRIBUTES][ROOM]] + return str(self._diffuser.room_size_square_meter) async def async_select_option(self, option: str) -> None: """Change the diffuser room size.""" if option in self.options: - await self._diffuser.set_room_size( - { - "15": 1, - "30": 2, - "60": 3, - "100": 4, - }[option] - ) + await self._diffuser.set_room_size_square_meter(int(option)) else: raise ValueError( f"Can't set the room size to {option}. Allowed room sizes are: {self.options}" diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 31a04bb5b8f..110a58fcd13 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -13,12 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS +from .const import COORDINATORS, DEVICES, DOMAIN, SENSORS from .entity import DiffuserEntity -TITLE = "title" -ICON = "icon" -WIFI = "wific" +ID = "id" PERFUME = "rfidc" FILL = "fillc" @@ -30,8 +28,6 @@ PERFUME_SUFFIX = " Perfume" FILL_SUFFIX = " Fill" WIFI_SUFFIX = " Wifi" -ATTR_SIGNAL_STRENGTH = "signal_strength" - async def async_setup_entry( hass: HomeAssistant, @@ -72,7 +68,7 @@ class DiffuserPerfumeSensor(DiffuserEntity): @property def state(self) -> str: """Return the state of the perfume sensor.""" - return self._diffuser.hub_data[SENSORS][PERFUME][TITLE] + return self._diffuser.perfume class DiffuserFillSensor(DiffuserEntity): @@ -94,7 +90,7 @@ class DiffuserFillSensor(DiffuserEntity): @property def state(self) -> str: """Return the state of the fill sensor.""" - return self._diffuser.hub_data[SENSORS][FILL][TITLE] + return self._diffuser.fill class DiffuserBatterySensor(DiffuserEntity): diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 54e985af77f..364b29ef1fb 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RitualsDataUpdateCoordinator -from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, ROOM, SPEED +from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity FAN = "fanc" @@ -54,8 +54,8 @@ class DiffuserSwitch(SwitchEntity, DiffuserEntity): def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { - "fan_speed": self._diffuser.hub_data[ATTRIBUTES][SPEED], - "room_size": self._diffuser.hub_data[ATTRIBUTES][ROOM], + "fan_speed": self._diffuser.perfume_amount, + "room_size": self._diffuser.room_size, } return attributes From d6fd7dde7f8dfafc45b067977251e9c696526f0c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 28 Jun 2021 18:49:40 -0400 Subject: [PATCH 615/750] Bump zwave_js_server to 0.27.0 (#52267) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index fd342b8d498..a48d0513c17 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.26.1"], + "requirements": ["zwave-js-server-python==0.27.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 8e422d67a16..3f6d3e5ccec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,4 +2454,4 @@ zigpy==0.34.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.1 +zwave-js-server-python==0.27.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22efc23a38b..6dfec7520a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1348,4 +1348,4 @@ zigpy-znp==0.5.1 zigpy==0.34.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.26.1 +zwave-js-server-python==0.27.0 From a448b72ff9997afc4b430ad15469ca7f1792361d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 28 Jun 2021 18:41:02 -0500 Subject: [PATCH 616/750] Remove bachya as 17track.net codeowner (#52262) * Remove bachya as 17track.net codeowner * Cleanup --- CODEOWNERS | 1 - homeassistant/components/seventeentrack/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f47cf29d6c8..70fbaf8ce1f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -432,7 +432,6 @@ homeassistant/components/sensibo/* @andrey-git homeassistant/components/sentry/* @dcramer @frenck homeassistant/components/serial/* @fabaff homeassistant/components/seven_segments/* @fabaff -homeassistant/components/seventeentrack/* @bachya homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 6f0ed4c8a9d..15a94a4230f 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -3,6 +3,6 @@ "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": ["py17track==3.2.1"], - "codeowners": ["@bachya"], + "codeowners": [], "iot_class": "cloud_polling" } From 81ca5236bf4b1a5cb4bd996210eebcbe506589fc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 29 Jun 2021 00:25:47 +0000 Subject: [PATCH 617/750] [ci skip] Translation update --- .../cloudflare/translations/de.json | 7 ++++ .../cloudflare/translations/en.json | 7 ++++ .../cloudflare/translations/et.json | 7 ++++ .../cloudflare/translations/ru.json | 7 ++++ .../components/coinbase/translations/en.json | 2 +- .../components/coinbase/translations/et.json | 40 +++++++++++++++++++ .../components/coinbase/translations/ru.json | 40 +++++++++++++++++++ .../devolo_home_control/translations/et.json | 6 ++- .../devolo_home_control/translations/ru.json | 6 ++- .../forecast_solar/translations/de.json | 31 ++++++++++++++ .../nmap_tracker/translations/et.json | 38 ++++++++++++++++++ .../nmap_tracker/translations/ru.json | 38 ++++++++++++++++++ .../nmap_tracker/translations/zh-Hans.json | 35 ++++++++++++++++ .../components/onvif/translations/en.json | 15 +++++++ .../components/onvif/translations/et.json | 13 ++++++ .../components/onvif/translations/ru.json | 13 ++++++ .../philips_js/translations/de.json | 9 +++++ .../philips_js/translations/et.json | 9 +++++ .../philips_js/translations/ru.json | 9 +++++ 19 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/et.json create mode 100644 homeassistant/components/coinbase/translations/ru.json create mode 100644 homeassistant/components/forecast_solar/translations/de.json create mode 100644 homeassistant/components/nmap_tracker/translations/et.json create mode 100644 homeassistant/components/nmap_tracker/translations/ru.json create mode 100644 homeassistant/components/nmap_tracker/translations/zh-Hans.json diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 5bd4631d5b3..d03f293b38b 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API-Token", + "description": "Authentifiziere dich erneut mit deinem Cloudflare-Konto." + } + }, "records": { "data": { "records": "Datens\u00e4tze" diff --git a/homeassistant/components/cloudflare/translations/en.json b/homeassistant/components/cloudflare/translations/en.json index 3dcd60ac4a9..43034cfbb4a 100644 --- a/homeassistant/components/cloudflare/translations/en.json +++ b/homeassistant/components/cloudflare/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Re-authentication was successful", "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown": "Unexpected error" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token", + "description": "Re-authenticate with your Cloudflare account." + } + }, "records": { "data": { "records": "Records" diff --git a/homeassistant/components/cloudflare/translations/et.json b/homeassistant/components/cloudflare/translations/et.json index 1688372fa1e..779706b29a4 100644 --- a/homeassistant/components/cloudflare/translations/et.json +++ b/homeassistant/components/cloudflare/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "unknown": "Tundmatu viga" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API v\u00f5ti", + "description": "Taastuvasta oma Cloudflare'i kontoga." + } + }, "records": { "data": { "records": "Kirjed" diff --git a/homeassistant/components/cloudflare/translations/ru.json b/homeassistant/components/cloudflare/translations/ru.json index 2b8eb0b140a..5b4eb7e67a3 100644 --- a/homeassistant/components/cloudflare/translations/ru.json +++ b/homeassistant/components/cloudflare/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API", + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Cloudflare." + } + }, "records": { "data": { "records": "\u0417\u0430\u043f\u0438\u0441\u0438" diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index b72cd236fa4..e17d9c07c5a 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -37,4 +37,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json new file mode 100644 index 00000000000..7b58288d98a --- /dev/null +++ b/homeassistant/components/coinbase/translations/et.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamtu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "api_token": "API salas\u00f5na", + "currencies": "Konto saldo valuutad", + "exchange_rates": "Vahetuskursid" + }, + "description": "Sisesta Coinbase'i pakutavad API-v\u00f5tme \u00fcksikasjad. Eralda mitu valuutat komaga (nt \"BTC, EUR\")", + "title": "Coinbase'i API v\u00f5tme \u00fcksikasjad" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Coinbase'i API ei paku \u00fchte v\u00f5i mitut taotletud valuutasaldot.", + "exchange_rate_unavaliable": "\u00dchte v\u00f5i mitut taotletud vahetuskurssi Coinbase ei paku.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Rahakoti saldod teavitamine.", + "exchange_rate_currencies": "Vahetuskursside aruanne." + }, + "description": "Kohanda Coinbase'i valikuid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json new file mode 100644 index 00000000000..1d14f56389b --- /dev/null +++ b/homeassistant/components/coinbase/translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "api_token": "\u0421\u0435\u043a\u0440\u0435\u0442 API", + "currencies": "\u041e\u0441\u0442\u0430\u0442\u043e\u043a \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0430 \u0441\u0447\u0435\u0442\u0435", + "exchange_rates": "\u041e\u0431\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u0443\u0440\u0441\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0435 Coinbase. \u0420\u0430\u0437\u0434\u0435\u043b\u044f\u0439\u0442\u0435 \u0432\u0430\u043b\u044e\u0442\u044b \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \"BTC, EUR\").", + "title": "\u041a\u043b\u044e\u0447 API Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.", + "exchange_rate_unavaliable": "Coinbase \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u0443\u0440\u0441\u043e\u0432.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "\u0411\u0430\u043b\u0430\u043d\u0441\u044b \u043a\u043e\u0448\u0435\u043b\u044c\u043a\u0430 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438.", + "exchange_rate_currencies": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438." + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json index 75d332456e5..8997f4952df 100644 --- a/homeassistant/components/devolo_home_control/translations/et.json +++ b/homeassistant/components/devolo_home_control/translations/et.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "invalid_auth": "Tuvastamise viga" + "invalid_auth": "Tuvastamise viga", + "reauth_failed": "Palun kasuta sama mydevolo kasutajat nagu varem." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index 7334ba2ad38..ab0463bc811 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "reauth_failed": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f mydevolo, \u0447\u0442\u043e \u0438 \u0440\u0430\u043d\u044c\u0448\u0435." }, "step": { "user": { diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json new file mode 100644 index 00000000000..86b51a14845 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)", + "declination": "Deklination (0 = Horizontal, 90 = Vertikal)", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule", + "name": "Name" + }, + "description": "Gib die Daten deiner Solarmodule ein. Wenn ein Feld unklar ist, schlage bitte in der Dokumentation nach." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API-Schl\u00fcssel (optional)", + "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)", + "damping": "D\u00e4mpfungsfaktor: passt die Ergebnisse morgens und abends an", + "declination": "Deklination (0 = Horizontal, 90 = Vertikal)", + "modules power": "Gesamt-Watt-Spitzenleistung Ihrer Solarmodule" + }, + "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/et.json b/homeassistant/components/nmap_tracker/translations/et.json new file mode 100644 index 00000000000..8b98dbc87cc --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/et.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "invalid_hosts": "Sobimatud hostid" + }, + "step": { + "user": { + "data": { + "exclude": "V\u00f5rguaadressid (komadega eraldatud), mis tuleb skaneerimisest v\u00e4lja j\u00e4tta", + "home_interval": "Minimaalne minutite arv aktiivsete seadmete skaneerimise vahel (aku s\u00e4ilitamine)", + "hosts": "Skaneeritavad v\u00f5rgu aadressid (komadega eraldatud)", + "scan_options": "Nmapi algseadistavad skaneerimisvalikud" + }, + "description": "Konfigureeri hostid, mida Nmap skannib. V\u00f5rguaadress ja v\u00e4ljaj\u00e4etud v\u00f5ivad olla IP-aadressid (192.168.1.1), IP-v\u00f5rgud (192.168.0.0/24) v\u00f5i IP-vahemikud (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Vigased hostid" + }, + "step": { + "init": { + "data": { + "exclude": "V\u00e4listatud IP aadresside vahemik (komadega eraldatud list)", + "home_interval": "Minimaalne sk\u00e4nnimiste intervall minutites (eeldus on aku s\u00e4\u00e4stmine)", + "hosts": "V\u00f5rguaadresside vahemik (komaga eraldatud)", + "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud" + }, + "description": "Vali Nmap poolt sk\u00e4nnitavad hostid. Valikuks on IP aadressid (192.168.1.1), v\u00f5rgud (192.168.0.0/24) v\u00f5i IP vahemikud (192.168.1.0-32)." + } + } + }, + "title": "Nmap asukoht" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/ru.json b/homeassistant/components/nmap_tracker/translations/ru.json new file mode 100644 index 00000000000..dc488f64ca6 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_hosts": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b." + }, + "step": { + "user": { + "data": { + "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", + "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)", + "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", + "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b." + }, + "step": { + "init": { + "data": { + "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", + "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)", + "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", + "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hans.json b/homeassistant/components/nmap_tracker/translations/zh-Hans.json new file mode 100644 index 00000000000..e0ca0563b7a --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/zh-Hans.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "invalid_hosts": "\u4e3b\u673a\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "exclude": "\u4ece\u626b\u63cf\u4e2d\u6392\u9664\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", + "home_interval": "\u626b\u63cf\u8bbe\u5907\u7684\u6700\u5c0f\u95f4\u9694\u5206\u949f\u6570\uff08\u7528\u4e8e\u8282\u7701\u7535\u91cf\uff09", + "hosts": "\u8981\u626b\u63cf\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", + "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879" + }, + "description": "\u914d\u7f6e\u901a\u8fc7 Nmap \u626b\u63cf\u7684\u4e3b\u673a\u3002\u7f51\u7edc\u5730\u5740\u548c\u6392\u9664\u9879\u53ef\u4ee5\u662f IP \u5730\u5740 (192.168.1.1)\u3001IP \u5730\u5740\u5757 (192.168.0.0/24) \u6216 IP \u8303\u56f4 (192.168.1.0-32)\u3002" + } + } + }, + "options": { + "error": { + "invalid_hosts": "\u4e3b\u673a\u65e0\u6548" + }, + "step": { + "init": { + "data": { + "exclude": "\u4ece\u626b\u63cf\u4e2d\u6392\u9664\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", + "home_interval": "\u626b\u63cf\u8bbe\u5907\u7684\u6700\u5c0f\u95f4\u9694\u5206\u949f\u6570\uff08\u7528\u4e8e\u8282\u7701\u7535\u91cf\uff09", + "hosts": "\u8981\u626b\u63cf\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", + "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879" + }, + "description": "\u914d\u7f6e\u901a\u8fc7 Nmap \u626b\u63cf\u7684\u4e3b\u673a\u3002\u7f51\u7edc\u5730\u5740\u548c\u6392\u9664\u9879\u53ef\u4ee5\u662f IP \u5730\u5740 (192.168.1.1)\u3001IP \u5730\u5740\u5757 (192.168.0.0/24) \u6216 IP \u8303\u56f4 (192.168.1.0-32)\u3002" + } + } + }, + "title": "Nmap \u8ddf\u8e2a\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index c3b328646ee..c922fc18482 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -11,6 +11,13 @@ "cannot_connect": "Failed to connect" }, "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Configure authentication" + }, "configure": { "data": { "host": "Host", @@ -34,6 +41,14 @@ }, "title": "Select ONVIF device" }, + "manual_input": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "title": "Configure ONVIF device" + }, "user": { "data": { "auto": "Search automatically" diff --git a/homeassistant/components/onvif/translations/et.json b/homeassistant/components/onvif/translations/et.json index 9ba14ffaa52..61eefa84d12 100644 --- a/homeassistant/components/onvif/translations/et.json +++ b/homeassistant/components/onvif/translations/et.json @@ -18,6 +18,16 @@ }, "title": "Autentimise seadistamine" }, + "configure": { + "data": { + "host": "Host", + "name": "Nimi", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "title": "Seadista ONVIF-seade" + }, "configure_profile": { "data": { "include": "Loo kaamera olem" @@ -40,6 +50,9 @@ "title": "H\u00e4\u00e4lesta ONVIF-seade" }, "user": { + "data": { + "auto": "Otsi automaatselt" + }, "description": "Kl\u00f5psates nuppu Esita, otsime v\u00f5rgust ONVIF-seadmeid, mis toetavad Profile S'i.\n\n M\u00f5ned tootjad on ONVIF-i vaikimisi keelanud. Veendu, et ONVIF on kaamera seadistustes lubatud.", "title": "ONVIF-seadme seadistamine" } diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json index 6b486bb62e2..55d6549645d 100644 --- a/homeassistant/components/onvif/translations/ru.json +++ b/homeassistant/components/onvif/translations/ru.json @@ -18,6 +18,16 @@ }, "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" }, + "configure": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + }, "configure_profile": { "data": { "include": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b" @@ -40,6 +50,9 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" }, "user": { + "data": { + "auto": "\u0418\u0441\u043a\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438" + }, "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" } diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json index 552d7aca07a..67d87b32001 100644 --- a/homeassistant/components/philips_js/translations/de.json +++ b/homeassistant/components/philips_js/translations/de.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Ger\u00e4t wird zum Einschalten aufgefordert" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Nutzung des Datenbenachrichtigungsdienstes zulassen." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json index 4a5bf9fe6e9..a089b7b0e57 100644 --- a/homeassistant/components/philips_js/translations/et.json +++ b/homeassistant/components/philips_js/translations/et.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Seadmel palutakse sisse l\u00fclituda" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Luba teavitusteenused." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json index df3dfd4b6f6..76597a21a9f 100644 --- a/homeassistant/components/philips_js/translations/ru.json +++ b/homeassistant/components/philips_js/translations/ru.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445" + } + } + } } } \ No newline at end of file From 1676bf220f187b808f874ff3eac654381bc5a147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 29 Jun 2021 05:58:27 +0200 Subject: [PATCH 618/750] Tibber, add device class monetary to accumulated cost (#52259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index c48a201d796..bf6218dcb31 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -9,6 +9,7 @@ import aiohttp from homeassistant.components.sensor import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -128,7 +129,12 @@ RT_SENSOR_MAP = { SIGNAL_STRENGTH_DECIBELS, STATE_CLASS_MEASUREMENT, ], - "accumulatedCost": ["accumulated cost", None, None, STATE_CLASS_MEASUREMENT], + "accumulatedCost": [ + "accumulated cost", + DEVICE_CLASS_MONETARY, + None, + STATE_CLASS_MEASUREMENT, + ], "powerFactor": [ "power factor", DEVICE_CLASS_POWER_FACTOR, From 74e1600a84e56027b364fd46959581ec7974417b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 29 Jun 2021 07:21:04 +0200 Subject: [PATCH 619/750] Add fixture to handle mock restore state (#52198) --- tests/components/modbus/conftest.py | 9 +- tests/components/modbus/test_binary_sensor.py | 53 ++++++----- tests/components/modbus/test_climate.py | 63 +++++++------ tests/components/modbus/test_cover.py | 82 +++++++++-------- tests/components/modbus/test_fan.py | 82 +++++++++-------- tests/components/modbus/test_light.py | 82 +++++++++-------- tests/components/modbus/test_sensor.py | 69 +++++++-------- tests/components/modbus/test_switch.py | 88 +++++++++---------- 8 files changed, 259 insertions(+), 269 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 2902f0f7675..db960f448ff 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache TEST_MODBUS_NAME = "modbusTest" _LOGGER = logging.getLogger(__name__) @@ -61,6 +61,13 @@ async def mock_modbus(hass, do_config): yield mock_pb +@pytest.fixture +async def mock_test_state(hass, request): + """Mock restore cache.""" + mock_restore_cache(hass, request.param) + return request.param + + # dataclass class ReadResult: """Storage class for register read results.""" diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 17f5dfb87a4..0f388edb3e8 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -19,9 +19,10 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import mock_restore_cache +sensor_name = "test_binary_sensor" +entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" @pytest.mark.parametrize( @@ -30,7 +31,7 @@ from tests.common import mock_restore_cache { CONF_BINARY_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, } ] @@ -38,7 +39,7 @@ from tests.common import mock_restore_cache { CONF_BINARY_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, @@ -85,7 +86,6 @@ async def test_config_binary_sensor(hass, mock_modbus): ) async def test_all_binary_sensor(hass, do_type, regs, expected): """Run test for given config.""" - sensor_name = "modbus_test_binary_sensor" state = await base_test( hass, {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, @@ -104,11 +104,10 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): async def test_service_binary_sensor_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "binary_sensor.test" config = { CONF_BINARY_SENSORS: [ { - CONF_NAME: "test", + CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -132,24 +131,24 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): assert hass.states.get(entity_id).state == STATE_ON -async def test_restore_state_binary_sensor(hass): +@pytest.mark.parametrize( + "mock_test_state", + [(State(entity_id, STATE_ON),)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: sensor_name, + CONF_ADDRESS: 51, + } + ] + }, + ], +) +async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus): """Run test for binary sensor restore state.""" - - sensor_name = "test_binary_sensor" - test_value = STATE_ON - config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} - mock_restore_cache( - hass, - (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), - ) - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_BINARY_SENSORS, - None, - method_discovery=True, - ) - entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" - assert hass.states.get(entity_id).state == test_value + assert hass.states.get(entity_id).state == mock_test_state[0].state diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 0f02470d10a..adbf5897be3 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -14,9 +14,10 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import mock_restore_cache +climate_name = "test_climate" +entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" @pytest.mark.parametrize( @@ -25,7 +26,7 @@ from tests.common import mock_restore_cache { CONF_CLIMATES: [ { - CONF_NAME: "test_climate", + CONF_NAME: climate_name, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -35,7 +36,7 @@ from tests.common import mock_restore_cache { CONF_CLIMATES: [ { - CONF_NAME: "test_climate", + CONF_NAME: climate_name, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -88,11 +89,10 @@ async def test_temperature_climate(hass, regs, expected): async def test_service_climate_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "climate.test" config = { CONF_CLIMATES: [ { - CONF_NAME: "test", + CONF_NAME: climate_name, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -110,32 +110,31 @@ async def test_service_climate_update(hass, mock_pymodbus): assert hass.states.get(entity_id).state == "auto" -async def test_restore_state_climate(hass): - """Run test for sensor restore state.""" +test_value = State(entity_id, 35) +test_value.attributes = {ATTR_TEMPERATURE: 37} - climate_name = "test_climate" - test_temp = 37 - entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" - test_value = State(entity_id, 35) - test_value.attributes = {ATTR_TEMPERATURE: test_temp} - config_sensor = { - CONF_NAME: climate_name, - CONF_TARGET_TEMP: 117, - CONF_ADDRESS: 117, - } - mock_restore_cache( - hass, - (test_value,), - ) - await base_config_test( - hass, - config_sensor, - climate_name, - CLIMATE_DOMAIN, - CONF_CLIMATES, - None, - method_discovery=True, - ) + +@pytest.mark.parametrize( + "mock_test_state", + [(test_value,)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: climate_name, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + } + ], + }, + ], +) +async def test_restore_state_climate(hass, mock_test_state, mock_modbus): + """Run test for sensor restore state.""" state = hass.states.get(entity_id) assert state.state == HVAC_MODE_AUTO - assert state.attributes[ATTR_TEMPERATURE] == test_temp + assert state.attributes[ATTR_TEMPERATURE] == 37 diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index e2f58173442..c7b5d827788 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -29,9 +29,10 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import mock_restore_cache +cover_name = "test_cover" +entity_id = f"{COVER_DOMAIN}.{cover_name}" @pytest.mark.parametrize( @@ -40,7 +41,7 @@ from tests.common import mock_restore_cache { CONF_COVERS: [ { - CONF_NAME: "test_cover", + CONF_NAME: cover_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -49,7 +50,7 @@ from tests.common import mock_restore_cache { CONF_COVERS: [ { - CONF_NAME: "test_cover", + CONF_NAME: cover_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SLAVE: 10, @@ -91,7 +92,6 @@ async def test_config_cover(hass, mock_modbus): ) async def test_coil_cover(hass, regs, expected): """Run test for given config.""" - cover_name = "modbus_test_cover" state = await base_test( hass, { @@ -139,7 +139,6 @@ async def test_coil_cover(hass, regs, expected): ) async def test_register_cover(hass, regs, expected): """Run test for given config.""" - cover_name = "modbus_test_cover" state = await base_test( hass, { @@ -162,11 +161,10 @@ async def test_register_cover(hass, regs, expected): async def test_service_cover_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "cover.test" config = { CONF_COVERS: [ { - CONF_NAME: "test", + CONF_NAME: cover_name, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, } @@ -189,54 +187,54 @@ async def test_service_cover_update(hass, mock_pymodbus): @pytest.mark.parametrize( - "state", [STATE_CLOSED, STATE_CLOSING, STATE_OPENING, STATE_OPEN] + "mock_test_state", + [ + (State(entity_id, STATE_CLOSED),), + (State(entity_id, STATE_CLOSING),), + (State(entity_id, STATE_OPENING),), + (State(entity_id, STATE_OPEN),), + ], + indirect=True, ) -async def test_restore_state_cover(hass, state): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: cover_name, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, + CONF_STATE_OPEN: 1, + CONF_STATE_CLOSED: 0, + CONF_STATE_OPENING: 2, + CONF_STATE_CLOSING: 3, + CONF_STATUS_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + }, + ], +) +async def test_restore_state_cover(hass, mock_test_state, mock_modbus): """Run test for cover restore state.""" - - entity_id = "cover.test" - cover_name = "test" - config = { - CONF_NAME: cover_name, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_ADDRESS: 1234, - CONF_STATE_OPEN: 1, - CONF_STATE_CLOSED: 0, - CONF_STATE_OPENING: 2, - CONF_STATE_CLOSING: 3, - CONF_STATUS_REGISTER: 1234, - CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - } - mock_restore_cache( - hass, - (State(f"{entity_id}", state),), - ) - await base_config_test( - hass, - config, - cover_name, - COVER_DOMAIN, - CONF_COVERS, - None, - method_discovery=True, - ) - assert hass.states.get(entity_id).state == state + test_state = mock_test_state[0].state + assert hass.states.get(entity_id).state == test_state async def test_service_cover_move(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "cover.test" - entity_id2 = "cover.test2" + entity_id2 = f"{entity_id}2" config = { CONF_COVERS: [ { - CONF_NAME: "test", + CONF_NAME: cover_name, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: "test2", + CONF_NAME: f"{cover_name}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, }, diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 1f24bf7231c..d5420000e3e 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -32,9 +32,10 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import mock_restore_cache +fan_name = "test_fan" +entity_id = f"{FAN_DOMAIN}.{fan_name}" @pytest.mark.parametrize( @@ -43,7 +44,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, } ] @@ -51,7 +52,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -60,7 +61,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -77,7 +78,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -94,7 +95,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -111,7 +112,7 @@ from tests.common import mock_restore_cache { CONF_FANS: [ { - CONF_NAME: "test_fan", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -160,7 +161,6 @@ async def test_config_fan(hass, mock_modbus): ) async def test_all_fan(hass, call_type, regs, verify, expected): """Run test for given config.""" - fan_name = "modbus_test_fan" state = await base_test( hass, { @@ -182,34 +182,33 @@ async def test_all_fan(hass, call_type, regs, verify, expected): assert state == expected -async def test_restore_state_fan(hass): +@pytest.mark.parametrize( + "mock_test_state", + [(State(entity_id, STATE_ON),)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_FANS: [ + { + CONF_NAME: fan_name, + CONF_ADDRESS: 1234, + } + ] + }, + ], +) +async def test_restore_state_fan(hass, mock_test_state, mock_modbus): """Run test for fan restore state.""" - - fan_name = "test_fan" - entity_id = f"{FAN_DOMAIN}.{fan_name}" - test_value = STATE_ON - config_fan = {CONF_NAME: fan_name, CONF_ADDRESS: 17} - mock_restore_cache( - hass, - (State(f"{entity_id}", test_value),), - ) - await base_config_test( - hass, - config_fan, - fan_name, - FAN_DOMAIN, - CONF_FANS, - None, - method_discovery=True, - ) - assert hass.states.get(entity_id).state == test_value + assert hass.states.get(entity_id).state == STATE_ON async def test_fan_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id1 = f"{FAN_DOMAIN}.fan1" - entity_id2 = f"{FAN_DOMAIN}.fan2" + entity_id2 = f"{FAN_DOMAIN}.{fan_name}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -217,12 +216,12 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_FANS: [ { - CONF_NAME: "fan1", + CONF_NAME: fan_name, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: "fan2", + CONF_NAME: f"{fan_name}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -234,17 +233,17 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( - "fan", "turn_on", service_data={"entity_id": entity_id1} + "fan", "turn_on", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_ON + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( - "fan", "turn_off", service_data={"entity_id": entity_id1} + "fan", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(entity_id2).state == STATE_OFF @@ -268,20 +267,19 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "fan", "turn_off", service_data={"entity_id": entity_id1} + "fan", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_service_fan_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "fan.test" config = { CONF_FANS: [ { - CONF_NAME: "test", + CONF_NAME: fan_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index a7826681678..51d418a771f 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -32,9 +32,10 @@ from homeassistant.const import ( from homeassistant.core import State from homeassistant.setup import async_setup_component -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import mock_restore_cache +light_name = "test_light" +entity_id = f"{LIGHT_DOMAIN}.{light_name}" @pytest.mark.parametrize( @@ -43,7 +44,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, } ] @@ -51,7 +52,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -60,7 +61,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -77,7 +78,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -94,7 +95,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -111,7 +112,7 @@ from tests.common import mock_restore_cache { CONF_LIGHTS: [ { - CONF_NAME: "test_light", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -160,7 +161,6 @@ async def test_config_light(hass, mock_modbus): ) async def test_all_light(hass, call_type, regs, verify, expected): """Run test for given config.""" - light_name = "modbus_test_light" state = await base_test( hass, { @@ -182,34 +182,33 @@ async def test_all_light(hass, call_type, regs, verify, expected): assert state == expected -async def test_restore_state_light(hass): +@pytest.mark.parametrize( + "mock_test_state", + [(State(entity_id, STATE_ON),)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_LIGHTS: [ + { + CONF_NAME: light_name, + CONF_ADDRESS: 1234, + } + ] + }, + ], +) +async def test_restore_state_light(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - - light_name = "test_light" - entity_id = f"{LIGHT_DOMAIN}.{light_name}" - test_value = STATE_ON - config_light = {CONF_NAME: light_name, CONF_ADDRESS: 17} - mock_restore_cache( - hass, - (State(f"{entity_id}", test_value),), - ) - await base_config_test( - hass, - config_light, - light_name, - LIGHT_DOMAIN, - CONF_LIGHTS, - None, - method_discovery=True, - ) - assert hass.states.get(entity_id).state == test_value + assert hass.states.get(entity_id).state == mock_test_state[0].state async def test_light_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id1 = f"{LIGHT_DOMAIN}.light1" - entity_id2 = f"{LIGHT_DOMAIN}.light2" + entity_id2 = f"{entity_id}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -217,12 +216,12 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_LIGHTS: [ { - CONF_NAME: "light1", + CONF_NAME: light_name, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: "light2", + CONF_NAME: f"{light_name}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -234,17 +233,17 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity_id1} + "light", "turn_on", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_ON + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity_id1} + "light", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(entity_id2).state == STATE_OFF @@ -268,20 +267,19 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity_id1} + "light", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_service_light_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "light.test" config = { CONF_LIGHTS: [ { - CONF_NAME: "test", + CONF_NAME: light_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index e581fd8fb1d..a5e83780660 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -38,7 +38,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update -from tests.common import mock_restore_cache +sensor_name = "test_sensor" +entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" @pytest.mark.parametrize( @@ -47,7 +48,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, } ] @@ -55,7 +56,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -71,7 +72,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -87,7 +88,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_NONE, @@ -97,7 +98,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_BYTE, @@ -107,7 +108,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD, @@ -117,7 +118,7 @@ from tests.common import mock_restore_cache { CONF_SENSORS: [ { - CONF_NAME: "test_sensor", + CONF_NAME: sensor_name, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD_BYTE, @@ -210,7 +211,6 @@ async def test_config_wrong_struct_sensor( ): """Run test for sensor with wrong struct.""" - sensor_name = "test_sensor" config_sensor = { CONF_NAME: sensor_name, **do_config, @@ -505,7 +505,6 @@ async def test_config_wrong_struct_sensor( async def test_all_sensor(hass, cfg, regs, expected): """Run test for sensor.""" - sensor_name = "modbus_test_sensor" state = await base_test( hass, {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg}, @@ -560,7 +559,6 @@ async def test_all_sensor(hass, cfg, regs, expected): async def test_struct_sensor(hass, cfg, regs, expected): """Run test for sensor struct.""" - sensor_name = "modbus_test_sensor" state = await base_test( hass, {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg}, @@ -576,27 +574,27 @@ async def test_struct_sensor(hass, cfg, regs, expected): assert state == expected -async def test_restore_state_sensor(hass): +@pytest.mark.parametrize( + "mock_test_state", + [(State(entity_id, "117"),)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: sensor_name, + CONF_ADDRESS: 51, + } + ] + }, + ], +) +async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - - sensor_name = "test_sensor" - test_value = "117" - config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} - mock_restore_cache( - hass, - (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), - ) - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - ) - entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" - assert hass.states.get(entity_id).state == test_value + assert hass.states.get(entity_id).state == mock_test_state[0].state @pytest.mark.parametrize( @@ -604,11 +602,11 @@ async def test_restore_state_sensor(hass): [ ( CONF_SWAP_WORD, - "Error in sensor modbus_test_sensor swap(word) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {sensor_name} swap(word) not possible due to the registers count: 1, needed: 2", ), ( CONF_SWAP_WORD_BYTE, - "Error in sensor modbus_test_sensor swap(word_byte) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {sensor_name} swap(word_byte) not possible due to the registers count: 1, needed: 2", ), ], ) @@ -616,7 +614,6 @@ async def test_swap_sensor_wrong_config( hass, caplog, swap_type, error_message, mock_pymodbus ): """Run test for sensor swap.""" - sensor_name = "modbus_test_sensor" config = { CONF_NAME: sensor_name, CONF_ADDRESS: 1234, @@ -642,12 +639,10 @@ async def test_swap_sensor_wrong_config( async def test_service_sensor_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - - entity_id = "sensor.test" config = { CONF_SENSORS: [ { - CONF_NAME: "test", + CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, } diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index eb4efbc048c..f305f192f38 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -39,9 +39,12 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ReadResult, base_config_test, base_test, prepare_service_update +from .conftest import ReadResult, base_test, prepare_service_update -from tests.common import async_fire_time_changed, mock_restore_cache +from tests.common import async_fire_time_changed + +switch_name = "test_switch" +entity_id = f"{SWITCH_DOMAIN}.{switch_name}" @pytest.mark.parametrize( @@ -50,7 +53,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, } ] @@ -58,7 +61,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -67,7 +70,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -85,7 +88,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -104,7 +107,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -122,7 +125,7 @@ from tests.common import async_fire_time_changed, mock_restore_cache { CONF_SWITCHES: [ { - CONF_NAME: "test_switch", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -173,7 +176,6 @@ async def test_config_switch(hass, mock_modbus): ) async def test_all_switch(hass, call_type, regs, verify, expected): """Run test for given config.""" - switch_name = "modbus_test_switch" state = await base_test( hass, { @@ -195,34 +197,33 @@ async def test_all_switch(hass, call_type, regs, verify, expected): assert state == expected -async def test_restore_state_switch(hass): +@pytest.mark.parametrize( + "mock_test_state", + [(State(entity_id, STATE_ON),)], + indirect=True, +) +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SWITCHES: [ + { + CONF_NAME: switch_name, + CONF_ADDRESS: 1234, + } + ] + }, + ], +) +async def test_restore_state_switch(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - - switch_name = "test_switch" - entity_id = f"{SWITCH_DOMAIN}.{switch_name}" - test_value = STATE_ON - config_switch = {CONF_NAME: switch_name, CONF_ADDRESS: 17} - mock_restore_cache( - hass, - (State(f"{entity_id}", test_value),), - ) - await base_config_test( - hass, - config_switch, - switch_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - None, - method_discovery=True, - ) - assert hass.states.get(entity_id).state == test_value + assert hass.states.get(entity_id).state == mock_test_state[0].state async def test_switch_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id1 = f"{SWITCH_DOMAIN}.switch1" - entity_id2 = f"{SWITCH_DOMAIN}.switch2" + entity_id2 = f"{SWITCH_DOMAIN}.{switch_name}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -230,12 +231,12 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_SWITCHES: [ { - CONF_NAME: "switch1", + CONF_NAME: switch_name, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: "switch2", + CONF_NAME: f"{switch_name}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -247,17 +248,17 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( - "switch", "turn_on", service_data={"entity_id": entity_id1} + "switch", "turn_on", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_ON + assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( - "switch", "turn_off", service_data={"entity_id": entity_id1} + "switch", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(entity_id2).state == STATE_OFF @@ -281,20 +282,19 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "switch", "turn_off", service_data={"entity_id": entity_id1} + "switch", "turn_off", service_data={"entity_id": entity_id} ) await hass.async_block_till_done() - assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_service_switch_update(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id = "switch.test" config = { CONF_SWITCHES: [ { - CONF_NAME: "test", + CONF_NAME: switch_name, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, @@ -319,10 +319,6 @@ async def test_service_switch_update(hass, mock_pymodbus): async def test_delay_switch(hass, mock_pymodbus): """Run test for switch verify delay.""" - - switch_name = "test_switch" - entity_id = f"{SWITCH_DOMAIN}.{switch_name}" - config = { MODBUS_DOMAIN: [ { From f1303e02ff7e47293ab1f675ad51ad299383d870 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 29 Jun 2021 07:21:31 +0200 Subject: [PATCH 620/750] Let climate use base_struct_schema. (#52154) --- homeassistant/components/modbus/__init__.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ed797891502..e60bbbda78b 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -99,7 +99,6 @@ from .const import ( DATA_TYPE_UINT, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, - DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, ) @@ -193,30 +192,13 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CLIMATE_SCHEMA = vol.All( cv.deprecated(CONF_DATA_COUNT, replacement_key=CONF_COUNT), - BASE_COMPONENT_SCHEMA.extend( + BASE_STRUCT_SCHEMA.extend( { - vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [ - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, - ] - ), vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Optional(CONF_COUNT, default=2): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( - [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] - ), - vol.Optional(CONF_PRECISION, default=1): cv.positive_int, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), - vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string, vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, - vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( - [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] - ), } ), ) From a93487f389e6377fdbaace890ffc22697c39045e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 08:37:33 +0200 Subject: [PATCH 621/750] Add state class support to SolarEdge (#52271) --- homeassistant/components/solaredge/const.py | 215 +++++++++++++------ homeassistant/components/solaredge/models.py | 21 ++ homeassistant/components/solaredge/sensor.py | 95 ++++---- 3 files changed, 217 insertions(+), 114 deletions(-) create mode 100644 homeassistant/components/solaredge/models.py diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 258eafff304..81d9bc5aebe 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -2,7 +2,11 @@ from datetime import timedelta import logging +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT +from homeassistant.util import dt as dt_util + +from .models import SolarEdgeSensor DOMAIN = "solaredge" @@ -23,64 +27,153 @@ ENERGY_DETAILS_DELAY = timedelta(minutes=15) SCAN_INTERVAL = timedelta(minutes=15) -# Supported overview sensor types: -# Key: ['json_key', 'name', unit, icon, default] -SENSOR_TYPES = { - "lifetime_energy": [ - "lifeTimeData", - "Lifetime energy", - ENERGY_WATT_HOUR, - "mdi:solar-power", - False, - ], - "energy_this_year": [ - "lastYearData", - "Energy this year", - ENERGY_WATT_HOUR, - "mdi:solar-power", - False, - ], - "energy_this_month": [ - "lastMonthData", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - False, - ], - "energy_today": [ - "lastDayData", - "Energy today", - ENERGY_WATT_HOUR, - "mdi:solar-power", - False, - ], - "current_power": [ - "currentPower", - "Current Power", - POWER_WATT, - "mdi:solar-power", - True, - ], - "site_details": [None, "Site details", None, None, False], - "meters": ["meters", "Meters", None, None, False], - "sensors": ["sensors", "Sensors", None, None, False], - "gateways": ["gateways", "Gateways", None, None, False], - "batteries": ["batteries", "Batteries", None, None, False], - "inverters": ["inverters", "Inverters", None, None, False], - "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False], - "solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False], - "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False], - "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False], - "purchased_power": ["Purchased", "Imported Power", None, "mdi:flash", False], - "production_power": ["Production", "Production Power", None, "mdi:flash", False], - "consumption_power": ["Consumption", "Consumption Power", None, "mdi:flash", False], - "selfconsumption_power": [ - "SelfConsumption", - "SelfConsumption Power", - None, - "mdi:flash", - False, - ], - "feedin_power": ["FeedIn", "Exported Power", None, "mdi:flash", False], - "storage_level": ["STORAGE", "Storage Level", PERCENTAGE, None, False], -} +# Supported overview sensors +SENSOR_TYPES = [ + SolarEdgeSensor( + key="lifetime_energy", + json_key="lifeTimeData", + name="Lifetime energy", + icon="mdi:solar-power", + last_reset=dt_util.utc_from_timestamp(0), + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ENERGY_WATT_HOUR, + ), + SolarEdgeSensor( + key="energy_this_year", + json_key="lastYearData", + name="Energy this year", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + unit_of_measurement=ENERGY_WATT_HOUR, + ), + SolarEdgeSensor( + key="energy_this_month", + json_key="lastMonthData", + name="Energy this month", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + unit_of_measurement=ENERGY_WATT_HOUR, + ), + SolarEdgeSensor( + key="energy_today", + json_key="lastDayData", + name="Energy today", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + unit_of_measurement=ENERGY_WATT_HOUR, + ), + SolarEdgeSensor( + key="current_power", + json_key="currentPower", + name="Current Power", + icon="mdi:solar-power", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=POWER_WATT, + ), + SolarEdgeSensor( + key="site_details", + name="Site details", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="meters", + json_key="meters", + name="Meters", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="sensors", + json_key="sensors", + name="Sensors", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="gateways", + json_key="gateways", + name="Gateways", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="batteries", + json_key="batteries", + name="Batteries", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="inverters", + json_key="inverters", + name="Inverters", + entity_registry_enabled_default=False, + ), + SolarEdgeSensor( + key="power_consumption", + json_key="LOAD", + name="Power Consumption", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="solar_power", + json_key="PV", + name="Solar Power", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + ), + SolarEdgeSensor( + key="grid_power", + json_key="GRID", + name="Grid Power", + entity_registry_enabled_default=False, + icon="mdi:power-plug", + ), + SolarEdgeSensor( + key="storage_power", + json_key="STORAGE", + name="Storage Power", + entity_registry_enabled_default=False, + icon="mdi:car-battery", + ), + SolarEdgeSensor( + key="purchased_power", + json_key="Purchased", + name="Imported Power", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="production_power", + json_key="Production", + name="Production Power", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="consumption_power", + json_key="Consumption", + name="Consumption Power", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="selfconsumption_power", + json_key="SelfConsumption", + name="SelfConsumption Power", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="feedin_power", + json_key="FeedIn", + name="Exported Power", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensor( + key="storage_level", + json_key="STORAGE", + name="Storage Level", + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PERCENTAGE, + ), +] diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py new file mode 100644 index 00000000000..f91db9ee9ff --- /dev/null +++ b/homeassistant/components/solaredge/models.py @@ -0,0 +1,21 @@ +"""Models for the SolarEdge integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class SolarEdgeSensor: + """Represents an SolarEdge Sensor.""" + + key: str + name: str + + json_key: str | None = None + device_class: str | None = None + entity_registry_enabled_default: bool = True + icon: str | None = None + last_reset: datetime | None = None + state_class: str | None = None + unit_of_measurement: str | None = None diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 5cd644f5006..340b6e0c2c9 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -21,6 +21,7 @@ from .coordinator import ( SolarEdgeOverviewDataService, SolarEdgePowerFlowDataService, ) +from .models import SolarEdgeSensor async def async_setup_entry( @@ -40,8 +41,8 @@ async def async_setup_entry( await service.coordinator.async_refresh() entities = [] - for sensor_key in SENSOR_TYPES: - sensor = sensor_factory.create_sensor(sensor_key) + for sensor_type in SENSOR_TYPES: + sensor = sensor_factory.create_sensor(sensor_type) if sensor is not None: entities.append(sensor) async_add_entities(entities) @@ -98,48 +99,49 @@ class SolarEdgeSensorFactory: ]: self.services[key] = (SolarEdgeEnergyDetailsSensor, energy) - def create_sensor(self, sensor_key: str) -> SolarEdgeSensor: + def create_sensor(self, sensor_type: SolarEdgeSensor) -> SolarEdgeSensor: """Create and return a sensor based on the sensor_key.""" - sensor_class, service = self.services[sensor_key] + sensor_class, service = self.services[sensor_type.key] - return sensor_class(self.platform_name, sensor_key, service) + return sensor_class(self.platform_name, sensor_type, service) -class SolarEdgeSensor(CoordinatorEntity, SensorEntity): +class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity): """Abstract class for a solaredge sensor.""" def __init__( - self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + self, + platform_name: str, + sensor_type: SolarEdgeSensor, + data_service: SolarEdgeDataService, ) -> None: """Initialize the sensor.""" super().__init__(data_service.coordinator) self.platform_name = platform_name - self.sensor_key = sensor_key + self.sensor_type = sensor_type self.data_service = data_service - self._attr_unit_of_measurement = SENSOR_TYPES[sensor_key][2] - self._attr_name = f"{platform_name} ({SENSOR_TYPES[sensor_key][1]})" - self._attr_icon = SENSOR_TYPES[sensor_key][3] + self._attr_device_class = sensor_type.device_class + self._attr_entity_registry_enabled_default = ( + sensor_type.entity_registry_enabled_default + ) + self._attr_icon = sensor_type.icon + self._attr_last_reset = sensor_type.last_reset + self._attr_name = f"{platform_name} ({sensor_type.name})" + self._attr_state_class = sensor_type.state_class + self._attr_unit_of_measurement = sensor_type.unit_of_measurement -class SolarEdgeOverviewSensor(SolarEdgeSensor): +class SolarEdgeOverviewSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API overview sensor.""" - def __init__( - self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService - ) -> None: - """Initialize the overview sensor.""" - super().__init__(platform_name, sensor_key, data_service) - - self._json_key = SENSOR_TYPES[self.sensor_key][0] - @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self._json_key) + return self.data_service.data.get(self.sensor_type.json_key) -class SolarEdgeDetailsSensor(SolarEdgeSensor): +class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API details sensor.""" @property @@ -153,89 +155,76 @@ class SolarEdgeDetailsSensor(SolarEdgeSensor): return self.data_service.data -class SolarEdgeInventorySensor(SolarEdgeSensor): +class SolarEdgeInventorySensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API inventory sensor.""" - def __init__(self, platform_name, sensor_key, data_service): - """Initialize the inventory sensor.""" - super().__init__(platform_name, sensor_key, data_service) - - self._json_key = SENSOR_TYPES[self.sensor_key][0] - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self._json_key) + return self.data_service.attributes.get(self.sensor_type.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self._json_key) + return self.data_service.data.get(self.sensor_type.json_key) -class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): +class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" - def __init__(self, platform_name, sensor_key, data_service): + def __init__(self, platform_name, sensor_type, data_service): """Initialize the power flow sensor.""" - super().__init__(platform_name, sensor_key, data_service) + super().__init__(platform_name, sensor_type, data_service) - self._json_key = SENSOR_TYPES[self.sensor_key][0] self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self._json_key) + return self.data_service.attributes.get(self.sensor_type.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self._json_key) + return self.data_service.data.get(self.sensor_type.json_key) -class SolarEdgePowerFlowSensor(SolarEdgeSensor): +class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" _attr_device_class = DEVICE_CLASS_POWER def __init__( - self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService + self, + platform_name: str, + sensor_type: SolarEdgeSensor, + data_service: SolarEdgeDataService, ) -> None: """Initialize the power flow sensor.""" - super().__init__(platform_name, sensor_key, data_service) + super().__init__(platform_name, sensor_type, data_service) - self._json_key = SENSOR_TYPES[self.sensor_key][0] self._attr_unit_of_measurement = data_service.unit @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - return self.data_service.attributes.get(self._json_key) + return self.data_service.attributes.get(self.sensor_type.json_key) @property def state(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data.get(self._json_key) + return self.data_service.data.get(self.sensor_type.json_key) -class SolarEdgeStorageLevelSensor(SolarEdgeSensor): +class SolarEdgeStorageLevelSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API storage level sensor.""" _attr_device_class = DEVICE_CLASS_BATTERY - def __init__( - self, platform_name: str, sensor_key: str, data_service: SolarEdgeDataService - ) -> None: - """Initialize the storage level sensor.""" - super().__init__(platform_name, sensor_key, data_service) - - self._json_key = SENSOR_TYPES[self.sensor_key][0] - @property def state(self) -> str | None: """Return the state of the sensor.""" - attr = self.data_service.attributes.get(self._json_key) + attr = self.data_service.attributes.get(self.sensor_type.json_key) if attr and "soc" in attr: return attr["soc"] return None From 94a025974335a97fe172775cf682187be7e29379 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 08:38:21 +0200 Subject: [PATCH 622/750] Add state class support to SAJ Solar Inverter (#52261) --- homeassistant/components/saj/sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index f1def71cc64..fb3b31764f8 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -1,11 +1,17 @@ """SAJ solar inverter interface.""" +from __future__ import annotations + from datetime import date import logging import pysaj import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -27,6 +33,7 @@ from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_call_later +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -169,6 +176,11 @@ class SAJsensor(SensorEntity): self._serialnumber = serialnumber self._state = self._sensor.value + if pysaj_sensor.name in ("current_power", "total_yield", "temperature"): + self._attr_state_class = STATE_CLASS_MEASUREMENT + if pysaj_sensor.name == "total_yield": + self._attr_last_reset = dt_util.utc_from_timestamp(0) + @property def name(self): """Return the name of the sensor.""" From d37018cf87d12192cdb1d991c6b46aa52db925f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 08:39:00 +0200 Subject: [PATCH 623/750] Small tweaks to Rituals Perfume Genie (#52269) --- .../rituals_perfume_genie/__init__.py | 4 +-- .../rituals_perfume_genie/binary_sensor.py | 18 ++++------ .../rituals_perfume_genie/entity.py | 34 ++++++------------ .../rituals_perfume_genie/number.py | 19 +++------- .../rituals_perfume_genie/sensor.py | 35 +++++-------------- .../rituals_perfume_genie/switch.py | 27 ++++---------- 6 files changed, 40 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index 114c7f6d400..84fc5ed2cf5 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -60,10 +60,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class RitualsDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" + """Class to manage fetching Rituals Perfume Genie device data from single endpoint.""" def __init__(self, hass: HomeAssistant, device: Diffuser) -> None: - """Initialize global Rituals Perufme Genie data updater.""" + """Initialize global Rituals Perfume Genie data updater.""" self._device = device super().__init__( hass, diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index 8446d899480..a529ff3dca6 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -26,18 +26,19 @@ async def async_setup_entry( """Set up the diffuser binary sensors.""" diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] - entities = [] - for hublot, diffuser in diffusers.items(): - if diffuser.has_battery: - coordinator = coordinators[hublot] - entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator)) - async_add_entities(entities) + async_add_entities( + DiffuserBatteryChargingBinarySensor(diffuser, coordinators[hublot]) + for hublot, diffuser in diffusers.items() + if diffuser.has_battery + ) class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): """Representation of a diffuser battery charging binary sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY_CHARGING + def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator ) -> None: @@ -48,8 +49,3 @@ class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): def is_on(self) -> bool: """Return the state of the battery charging binary sensor.""" return self._diffuser.charging - - @property - def device_class(self) -> str: - """Return the device class of the battery charging binary sensor.""" - return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index d03450609e7..19c3f3cd424 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from pyrituals import Diffuser -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RitualsDataUpdateCoordinator @@ -35,32 +34,21 @@ class DiffuserEntity(CoordinatorEntity): """Init from config, hookup diffuser and coordinator.""" super().__init__(coordinator) self._diffuser = diffuser - self._entity_suffix = entity_suffix - self._hublot = self._diffuser.hub_data[HUBLOT] - self._hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME] - @property - def unique_id(self) -> str: - """Return the unique ID of the entity.""" - return f"{self._hublot}{self._entity_suffix}" + hublot = self._diffuser.hub_data[HUBLOT] + hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME] - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._hubname}{self._entity_suffix}" + self._attr_name = f"{hubname}{entity_suffix}" + self._attr_unique_id = f"{hublot}{entity_suffix}" + self._attr_device_info = { + "name": hubname, + "identifiers": {(DOMAIN, hublot)}, + "manufacturer": MANUFACTURER, + "model": MODEL if diffuser.has_battery else MODEL2, + "sw_version": diffuser.hub_data[SENSORS][VERSION], + } @property def available(self) -> bool: """Return if the entity is available.""" return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - return { - "name": self._hubname, - "identifiers": {(DOMAIN, self._hublot)}, - "manufacturer": MANUFACTURER, - "model": MODEL if self._diffuser.has_battery else MODEL2, - "sw_version": self._diffuser.hub_data[SENSORS][VERSION], - } diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index bc9ac7c9f31..26ae393b071 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -37,32 +37,21 @@ async def async_setup_entry( class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity): """Representation of a diffuser perfume amount number.""" + _attr_icon = "mdi:gauge" + _attr_max_value = MAX_PERFUME_AMOUNT + _attr_min_value = MIN_PERFUME_AMOUNT + def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator ) -> None: """Initialize the diffuser perfume amount number.""" super().__init__(diffuser, coordinator, PERFUME_AMOUNT_SUFFIX) - @property - def icon(self) -> str: - """Return the icon of the perfume amount entity.""" - return "mdi:gauge" - @property def value(self) -> int: """Return the current perfume amount.""" return self._diffuser.perfume_amount - @property - def min_value(self) -> int: - """Return the minimum perfume amount.""" - return MIN_PERFUME_AMOUNT - - @property - def max_value(self) -> int: - """Return the maximum perfume amount.""" - return MAX_PERFUME_AMOUNT - async def async_set_value(self, value: float) -> None: """Set the perfume amount.""" if value.is_integer() and MIN_PERFUME_AMOUNT <= value <= MAX_PERFUME_AMOUNT: diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index 110a58fcd13..2965371733b 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -58,12 +58,9 @@ class DiffuserPerfumeSensor(DiffuserEntity): """Initialize the perfume sensor.""" super().__init__(diffuser, coordinator, PERFUME_SUFFIX) - @property - def icon(self) -> str: - """Return the perfume sensor icon.""" - if self._diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: - return "mdi:tag-remove" - return "mdi:tag-text" + self._attr_icon = "mdi:tag-text" + if diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: + self._attr_icon = "mdi:tag-remove" @property def state(self) -> str: @@ -96,6 +93,9 @@ class DiffuserFillSensor(DiffuserEntity): class DiffuserBatterySensor(DiffuserEntity): """Representation of a diffuser battery sensor.""" + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator ) -> None: @@ -107,20 +107,13 @@ class DiffuserBatterySensor(DiffuserEntity): """Return the state of the battery sensor.""" return self._diffuser.battery_percentage - @property - def device_class(self) -> str: - """Return the class of the battery sensor.""" - return DEVICE_CLASS_BATTERY - - @property - def unit_of_measurement(self) -> str: - """Return the battery unit of measurement.""" - return PERCENTAGE - class DiffuserWifiSensor(DiffuserEntity): """Representation of a diffuser wifi sensor.""" + _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_unit_of_measurement = PERCENTAGE + def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator ) -> None: @@ -131,13 +124,3 @@ class DiffuserWifiSensor(DiffuserEntity): def state(self) -> int: """Return the state of the wifi sensor.""" return self._diffuser.wifi_percentage - - @property - def device_class(self) -> str: - """Return the class of the wifi sensor.""" - return DEVICE_CLASS_SIGNAL_STRENGTH - - @property - def unit_of_measurement(self) -> str: - """Return the wifi unit of measurement.""" - return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 364b29ef1fb..924a38dfde8 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -14,10 +14,6 @@ from . import RitualsDataUpdateCoordinator from .const import COORDINATORS, DEVICES, DOMAIN from .entity import DiffuserEntity -FAN = "fanc" - -ON_STATE = "1" - async def async_setup_entry( hass: HomeAssistant, @@ -38,46 +34,37 @@ async def async_setup_entry( class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" + _attr_icon = "mdi:fan" + def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator ) -> None: """Initialize the diffuser switch.""" super().__init__(diffuser, coordinator, "") - self._is_on = self._diffuser.is_on - - @property - def icon(self) -> str: - """Return the icon of the device.""" - return "mdi:fan" + self._attr_is_on = self._diffuser.is_on @property def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" - attributes = { + return { "fan_speed": self._diffuser.perfume_amount, "room_size": self._diffuser.room_size, } - return attributes - - @property - def is_on(self) -> bool: - """If the device is currently on or off.""" - return self._is_on async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() - self._is_on = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._diffuser.turn_off() - self._is_on = False + self._attr_is_on = False self.async_write_ha_state() @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._is_on = self._diffuser.is_on + self._attr_is_on = self._diffuser.is_on self.async_write_ha_state() From 8dd545d060a200b734d6584a0ecf6b513bf79dca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 08:39:40 +0200 Subject: [PATCH 624/750] Demo: Sensor improvements (#52263) --- homeassistant/components/demo/sensor.py | 106 +++++++++--------------- 1 file changed, 40 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 00111d34dd4..817cbd435d1 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,5 +1,10 @@ """Demo platform that has a couple of fake sensors.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONCENTRATION_PARTS_PER_MILLION, @@ -10,11 +15,19 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, StateType from . import DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the Demo sensors.""" async_add_entities( [ @@ -58,7 +71,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Demo config entry.""" await async_setup_platform(hass, {}, async_add_entities) @@ -66,73 +83,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoSensor(SensorEntity): """Representation of a Demo sensor.""" + _attr_should_poll = False + def __init__( self, - unique_id, - name, - state, - device_class, - state_class, - unit_of_measurement, - battery, - ): + unique_id: str, + name: str, + state: StateType, + device_class: str | None, + state_class: str | None, + unit_of_measurement: str | None, + battery: StateType, + ) -> None: """Initialize the sensor.""" - self._battery = battery - self._device_class = device_class - self._name = name - self._state = state - self._state_class = state_class - self._unique_id = unique_id - self._unit_of_measurement = unit_of_measurement + self._attr_device_class = device_class + self._attr_name = name + self._attr_state = state + self._attr_state_class = state_class + self._attr_unique_id = unique_id + self._attr_unit_of_measurement = unit_of_measurement - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.unique_id) - }, - "name": self.name, + self._attr_device_info = { + "identifiers": {(DOMAIN, unique_id)}, + "name": name, } - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def should_poll(self): - """No polling needed for a demo sensor.""" - return False - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class - - @property - def state_class(self): - """Return the state class of the sensor.""" - return self._state_class - - @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 this state is expressed in.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return {ATTR_BATTERY_LEVEL: self._battery} + if battery: + self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} From bb4d3bfc606c80b1e5f3aa6431a86c926885cb18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Jun 2021 23:57:32 -0700 Subject: [PATCH 625/750] Reduce Ring TTL (#52277) --- homeassistant/components/ring/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 8f827aee7d2..580fc71e141 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -18,7 +18,7 @@ from homeassistant.util import dt as dt_util from . import ATTRIBUTION, DOMAIN from .entity import RingEntityMixin -FORCE_REFRESH_INTERVAL = timedelta(minutes=45) +FORCE_REFRESH_INTERVAL = timedelta(minutes=3) _LOGGER = logging.getLogger(__name__) From 3f66709882b315c618d4800a9d4e1d779b86f147 Mon Sep 17 00:00:00 2001 From: TOM <3240549+franc6@users.noreply.github.com> Date: Tue, 29 Jun 2021 02:07:29 -0500 Subject: [PATCH 626/750] Fix caldav TZ interpretation of all day events (#48642) --- homeassistant/components/caldav/calendar.py | 4 +- tests/components/caldav/test_calendar.py | 192 ++++++++++++++++---- 2 files changed, 156 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 62be361df3b..61186249c51 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -310,7 +310,9 @@ class WebDavCalendarData: # represent same time regardless of which time zone is currently being observed return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) return obj - return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min)) + return dt.dt.datetime.combine(obj, dt.dt.time.min).replace( + tzinfo=dt.DEFAULT_TIME_ZONE + ) @staticmethod def get_attr_value(obj, attribute): diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 5a5b7adf0e3..6993aa97081 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -222,6 +222,39 @@ CALDAV_CONFIG = { "custom_calendars": [], } +ORIG_TZ = dt.DEFAULT_TIME_ZONE + + +@pytest.fixture(autouse=True) +def reset_tz(): + """Restore the default TZ after test runs.""" + yield + dt.DEFAULT_TIME_ZONE = ORIG_TZ + + +@pytest.fixture +def set_tz(request): + """Set the default TZ to the one requested.""" + return request.getfixturevalue(request.param) + + +@pytest.fixture +def utc(): + """Set the default TZ to UTC.""" + dt.set_default_time_zone(dt.get_time_zone("UTC")) + + +@pytest.fixture +def new_york(): + """Set the default TZ to America/New_York.""" + dt.set_default_time_zone(dt.get_time_zone("America/New_York")) + + +@pytest.fixture +def baghdad(): + """Set the default TZ to Asia/Baghdad.""" + dt.set_default_time_zone(dt.get_time_zone("Asia/Baghdad")) + @pytest.fixture(autouse=True) def mock_http(hass): @@ -524,30 +557,72 @@ async def test_no_result_with_filtering(mock_now, hass, calendar): assert state.state == "off" -@patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 30)) -async def test_all_day_event_returned(mock_now, hass, calendar): - """Test that the event lasting the whole day is returned.""" +async def _day_event_returned(hass, calendar, config, date_time): + with patch("homeassistant.util.dt.now", return_value=date_time): + assert await async_setup_component(hass, "calendar", {"calendar": config}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.private_private") + assert state.name == calendar.name + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": "Private", + "message": "This is an all day event", + "all_day": True, + "offset_reached": False, + "start_time": "2017-11-27 00:00:00", + "end_time": "2017-11-28 00:00:00", + "location": "Hamburg", + "description": "What a beautiful day", + } + + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_all_day_event_returned_early(hass, calendar, set_tz): + """Test that the event lasting the whole day is returned, if it's early in the local day.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ {"name": "Private", "calendar": "Private", "search": ".*"} ] - assert await async_setup_component(hass, "calendar", {"calendar": config}) - await hass.async_block_till_done() + await _day_event_returned( + hass, + calendar, + config, + datetime.datetime(2017, 11, 27, 0, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) - state = hass.states.get("calendar.private_private") - assert state.name == calendar.name - assert state.state == STATE_ON - assert dict(state.attributes) == { - "friendly_name": "Private", - "message": "This is an all day event", - "all_day": True, - "offset_reached": False, - "start_time": "2017-11-27 00:00:00", - "end_time": "2017-11-28 00:00:00", - "location": "Hamburg", - "description": "What a beautiful day", - } + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_all_day_event_returned_mid(hass, calendar, set_tz): + """Test that the event lasting the whole day is returned, if it's in the middle of the local day.""" + config = dict(CALDAV_CONFIG) + config["custom_calendars"] = [ + {"name": "Private", "calendar": "Private", "search": ".*"} + ] + + await _day_event_returned( + hass, + calendar, + config, + datetime.datetime(2017, 11, 27, 12, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) + + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_all_day_event_returned_late(hass, calendar, set_tz): + """Test that the event lasting the whole day is returned, if it's late in the local day.""" + config = dict(CALDAV_CONFIG) + config["custom_calendars"] = [ + {"name": "Private", "calendar": "Private", "search": ".*"} + ] + + await _day_event_returned( + hass, + calendar, + config, + datetime.datetime(2017, 11, 27, 23, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) @patch("homeassistant.util.dt.now", return_value=_local_datetime(21, 45)) @@ -655,33 +730,72 @@ async def test_event_rrule_endless(mock_now, hass, calendar): } -@patch( - "homeassistant.util.dt.now", - return_value=dt.as_local(datetime.datetime(2016, 12, 1, 17, 30)), -) -async def test_event_rrule_all_day(mock_now, hass, calendar): - """Test that the recurring all day event is returned.""" +async def _event_rrule_all_day(hass, calendar, config, date_time): + with patch("homeassistant.util.dt.now", return_value=date_time): + assert await async_setup_component(hass, "calendar", {"calendar": config}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.private_private") + assert state.name == calendar.name + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": "Private", + "message": "This is a recurring all day event", + "all_day": True, + "offset_reached": False, + "start_time": "2016-12-01 00:00:00", + "end_time": "2016-12-02 00:00:00", + "location": "Hamburg", + "description": "Groundhog Day", + } + + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_event_rrule_all_day_early(hass, calendar, set_tz): + """Test that the recurring all day event is returned early in the local day, and not on the first occurrence.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ {"name": "Private", "calendar": "Private", "search": ".*"} ] - assert await async_setup_component(hass, "calendar", {"calendar": config}) - await hass.async_block_till_done() + await _event_rrule_all_day( + hass, + calendar, + config, + datetime.datetime(2016, 12, 1, 0, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) - state = hass.states.get("calendar.private_private") - assert state.name == calendar.name - assert state.state == STATE_ON - assert dict(state.attributes) == { - "friendly_name": "Private", - "message": "This is a recurring all day event", - "all_day": True, - "offset_reached": False, - "start_time": "2016-12-01 00:00:00", - "end_time": "2016-12-02 00:00:00", - "location": "Hamburg", - "description": "Groundhog Day", - } + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_event_rrule_all_day_mid(hass, calendar, set_tz): + """Test that the recurring all day event is returned in the middle of the local day, and not on the first occurrence.""" + config = dict(CALDAV_CONFIG) + config["custom_calendars"] = [ + {"name": "Private", "calendar": "Private", "search": ".*"} + ] + + await _event_rrule_all_day( + hass, + calendar, + config, + datetime.datetime(2016, 12, 1, 17, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) + + +@pytest.mark.parametrize("set_tz", ["utc", "new_york", "baghdad"], indirect=True) +async def test_event_rrule_all_day_late(hass, calendar, set_tz): + """Test that the recurring all day event is returned late in the local day, and not on the first occurrence.""" + config = dict(CALDAV_CONFIG) + config["custom_calendars"] = [ + {"name": "Private", "calendar": "Private", "search": ".*"} + ] + + await _event_rrule_all_day( + hass, + calendar, + config, + datetime.datetime(2016, 12, 1, 23, 30).replace(tzinfo=dt.DEFAULT_TIME_ZONE), + ) @patch( From 8c37dc5613fad78ff04bc4ecefde342cbc32fbc4 Mon Sep 17 00:00:00 2001 From: Xuefer Date: Tue, 29 Jun 2021 15:30:56 +0800 Subject: [PATCH 627/750] Clean up Onvif steps (#52254) --- homeassistant/components/onvif/config_flow.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index dd90712d43e..b4193f0def7 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -149,11 +149,15 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_configure() - async def async_step_configure(self, user_input=None, errors=None): + async def async_step_configure(self, user_input=None): """Device configuration.""" + errors = {} if user_input: self.onvif_config = user_input - return await self.async_step_profiles() + try: + return await self.async_setup_profiles() + except Fault: + errors["base"] = "cannot_connect" def conf(name, default=None): return self.onvif_config.get(name, default) @@ -176,10 +180,8 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_profiles(self, user_input=None): + async def async_setup_profiles(self): """Fetch ONVIF device profiles.""" - errors = {} - LOGGER.debug( "Fetching profiles from ONVIF device %s", pformat(self.onvif_config) ) @@ -256,18 +258,12 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="onvif_error") - except Fault: - errors["base"] = "cannot_connect" - finally: await device.close() - return await self.async_step_configure(errors=errors) - async def async_step_import(self, user_input): """Handle import.""" - self.onvif_config = user_input - return await self.async_step_profiles() + return await self.async_step_configure(user_input) class OnvifOptionsFlowHandler(config_entries.OptionsFlow): From 6a528acafe77a93aa8000b2627e2ea525e6c87ec Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 29 Jun 2021 03:02:49 -0500 Subject: [PATCH 628/750] Use attrs instead of properties for ipp (#52270) * use attrs instead of properties for ipp * Update __init__.py * Create entity.py * Update __init__.py * Create coordinator.py * Update coordinator.py * Update __init__.py * Update entity.py * Update sensor.py * Update sensor.py * Update __init__.py * Update __init__.py * Update coordinator.py * Update entity.py * Update entity.py * Update entity.py * Update sensor.py * Update sensor.py --- homeassistant/components/ipp/__init__.py | 118 +------------------- homeassistant/components/ipp/coordinator.py | 55 +++++++++ homeassistant/components/ipp/entity.py | 51 +++++++++ homeassistant/components/ipp/sensor.py | 20 +--- 4 files changed, 113 insertions(+), 131 deletions(-) create mode 100644 homeassistant/components/ipp/coordinator.py create mode 100644 homeassistant/components/ipp/entity.py diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index c1bc7ed4986..65d326f8f3a 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,40 +1,17 @@ """The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -from datetime import timedelta import logging -from pyipp import IPP, IPPError, Printer as IPPPrinter - from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - CONF_BASE_PATH, - DOMAIN, -) +from .const import CONF_BASE_PATH, DOMAIN +from .coordinator import IPPDataUpdateCoordinator PLATFORMS = [SENSOR_DOMAIN] -SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) @@ -68,92 +45,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): - """Class to manage fetching IPP data from single endpoint.""" - - def __init__( - self, - hass: HomeAssistant, - *, - host: str, - port: int, - base_path: str, - tls: bool, - verify_ssl: bool, - ) -> None: - """Initialize global IPP data updater.""" - self.ipp = IPP( - host=host, - port=port, - base_path=base_path, - tls=tls, - verify_ssl=verify_ssl, - session=async_get_clientsession(hass, verify_ssl), - ) - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - ) - - async def _async_update_data(self) -> IPPPrinter: - """Fetch data from IPP.""" - try: - return await self.ipp.printer() - except IPPError as error: - raise UpdateFailed(f"Invalid response from API: {error}") from error - - -class IPPEntity(CoordinatorEntity): - """Defines a base IPP entity.""" - - def __init__( - self, - *, - entry_id: str, - device_id: str, - coordinator: IPPDataUpdateCoordinator, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the IPP entity.""" - super().__init__(coordinator) - self._device_id = device_id - self._enabled_default = enabled_default - self._entry_id = entry_id - self._icon = icon - self._name = name - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this IPP device.""" - if self._device_id is None: - return None - - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.coordinator.data.info.name, - ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer, - ATTR_MODEL: self.coordinator.data.info.model, - ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, - } diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py new file mode 100644 index 00000000000..abc97dd3dd2 --- /dev/null +++ b/homeassistant/components/ipp/coordinator.py @@ -0,0 +1,55 @@ +"""Coordinator for The Internet Printing Protocol (IPP) integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pyipp import IPP, IPPError, Printer as IPPPrinter + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): + """Class to manage fetching IPP data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + *, + host: str, + port: int, + base_path: str, + tls: bool, + verify_ssl: bool, + ) -> None: + """Initialize global IPP data updater.""" + self.ipp = IPP( + host=host, + port=port, + base_path=base_path, + tls=tls, + verify_ssl=verify_ssl, + session=async_get_clientsession(hass, verify_ssl), + ) + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> IPPPrinter: + """Fetch data from IPP.""" + try: + return await self.ipp.printer() + except IPPError as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py new file mode 100644 index 00000000000..0038bbd7370 --- /dev/null +++ b/homeassistant/components/ipp/entity.py @@ -0,0 +1,51 @@ +"""Entities for The Internet Printing Protocol (IPP) integration.""" +from __future__ import annotations + +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + DOMAIN, +) +from .coordinator import IPPDataUpdateCoordinator + + +class IPPEntity(CoordinatorEntity): + """Defines a base IPP entity.""" + + def __init__( + self, + *, + entry_id: str, + device_id: str, + coordinator: IPPDataUpdateCoordinator, + name: str, + icon: str, + enabled_default: bool = True, + ) -> None: + """Initialize the IPP entity.""" + super().__init__(coordinator) + self._device_id = device_id + self._entry_id = entry_id + self._attr_name = name + self._attr_icon = icon + self._attr_entity_registry_enabled_default = enabled_default + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this IPP device.""" + if self._device_id is None: + return None + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: self.coordinator.data.info.name, + ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer, + ATTR_MODEL: self.coordinator.data.info.model, + ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + } diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index da56ada41f2..5d736c864e1 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -11,7 +11,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import IPPDataUpdateCoordinator, IPPEntity from .const import ( ATTR_COMMAND_SET, ATTR_INFO, @@ -24,6 +23,8 @@ from .const import ( ATTR_URI_SUPPORTED, DOMAIN, ) +from .coordinator import IPPDataUpdateCoordinator +from .entity import IPPEntity async def async_setup_entry( @@ -69,12 +70,9 @@ class IPPSensor(IPPEntity, SensorEntity): unit_of_measurement: str | None = None, ) -> None: """Initialize IPP sensor.""" - self._unit_of_measurement = unit_of_measurement self._key = key - self._unique_id = None - - if unique_id is not None: - self._unique_id = f"{unique_id}_{key}" + self._attr_unique_id = f"{unique_id}_{key}" + self._attr_unit_of_measurement = unit_of_measurement super().__init__( entry_id=entry_id, @@ -85,16 +83,6 @@ class IPPSensor(IPPEntity, SensorEntity): enabled_default=enabled_default, ) - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return self._unique_id - - @property - def unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - class IPPMarkerSensor(IPPSensor): """Defines an IPP marker sensor.""" From 3ab42c50c931e76dee42f8b29d6f66f7e885f0ad Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Tue, 29 Jun 2021 03:05:39 -0500 Subject: [PATCH 629/750] Add sensor platform to Modern Forms integration (#52249) * Add sensor platform to Modern Forms integration * Changes to sensors to timestamp class * lint cleanup --- .../components/modern_forms/__init__.py | 2 + .../components/modern_forms/sensor.py | 118 ++++++++++++++++++ tests/components/modern_forms/__init__.py | 12 ++ tests/components/modern_forms/test_sensor.py | 57 +++++++++ .../device_status_timers_active.json | 17 +++ 5 files changed, 206 insertions(+) create mode 100644 homeassistant/components/modern_forms/sensor.py create mode 100644 tests/components/modern_forms/test_sensor.py create mode 100644 tests/fixtures/modern_forms/device_status_timers_active.json diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index ca5e5388aaa..24195b96ea4 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -13,6 +13,7 @@ from aiomodernforms.models import Device as ModernFormsDeviceState from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, CONF_HOST @@ -31,6 +32,7 @@ SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ LIGHT_DOMAIN, FAN_DOMAIN, + SENSOR_DOMAIN, SWITCH_DOMAIN, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py new file mode 100644 index 00000000000..01efe3f1d28 --- /dev/null +++ b/homeassistant/components/modern_forms/sensor.py @@ -0,0 +1,118 @@ +"""Support for Modern Forms switches.""" +from __future__ import annotations + +from datetime import datetime + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util + +from . import ModernFormsDataUpdateCoordinator, ModernFormsDeviceEntity +from .const import CLEAR_TIMER, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Modern Forms sensor based on a config entry.""" + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + sensors: list[ModernFormsSensor] = [ + ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator), + ] + + # Only setup light sleep timer sensor if light unit installed + if coordinator.data.info.light_type: + sensors.append( + ModernFormsLightTimerRemainingTimeSensor(entry.entry_id, coordinator) + ) + + async_add_entities(sensors) + + +class ModernFormsSensor(ModernFormsDeviceEntity, SensorEntity): + """Defines a Modern Forms binary sensor.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: ModernFormsDataUpdateCoordinator, + name: str, + icon: str, + key: str, + ) -> None: + """Initialize Modern Forms switch.""" + self._key = key + super().__init__( + entry_id=entry_id, coordinator=coordinator, name=name, icon=icon + ) + self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" + + +class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): + """Defines the Modern Forms Light Timer remaining time sensor.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Away mode switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:timer-outline", + key="light_timer_remaining_time", + name=f"{coordinator.data.info.device_name} Light Sleep Time", + ) + self._attr_device_class = DEVICE_CLASS_TIMESTAMP + + @property + def state(self) -> StateType: + """Return the state of the sensor.""" + sleep_time: datetime = dt_util.utc_from_timestamp( + self.coordinator.data.state.light_sleep_timer + ) + if ( + self.coordinator.data.state.light_sleep_timer == CLEAR_TIMER + or (sleep_time - dt_util.utcnow()).total_seconds() < 0 + ): + return None + return sleep_time.isoformat() + + +class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): + """Defines the Modern Forms Light Timer remaining time sensor.""" + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Away mode switch.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:timer-outline", + key="fan_timer_remaining_time", + name=f"{coordinator.data.info.device_name} Fan Sleep Time", + ) + self._attr_device_class = DEVICE_CLASS_TIMESTAMP + + @property + def state(self) -> StateType: + """Return the state of the sensor.""" + sleep_time: datetime = dt_util.utc_from_timestamp( + self.coordinator.data.state.fan_sleep_timer + ) + + if ( + self.coordinator.data.state.fan_sleep_timer == CLEAR_TIMER + or (sleep_time - dt_util.utcnow()).total_seconds() < 0 + ): + return None + + return sleep_time.isoformat() diff --git a/tests/components/modern_forms/__init__.py b/tests/components/modern_forms/__init__.py index 54fcf53ce89..c6d2a8b3637 100644 --- a/tests/components/modern_forms/__init__.py +++ b/tests/components/modern_forms/__init__.py @@ -37,6 +37,18 @@ async def modern_forms_no_light_call_mock(method, url, data): return response +async def modern_forms_timers_set_mock(method, url, data): + """Set up the basic returns based on info or status request.""" + if COMMAND_QUERY_STATIC_DATA in data: + fixture = "modern_forms/device_info.json" + else: + fixture = "modern_forms/device_status_timers_active.json" + response = AiohttpClientMockResponse( + method=method, url=url, json=json.loads(load_fixture(fixture)) + ) + return response + + async def init_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/modern_forms/test_sensor.py b/tests/components/modern_forms/test_sensor.py new file mode 100644 index 00000000000..d18793f51c2 --- /dev/null +++ b/tests/components/modern_forms/test_sensor.py @@ -0,0 +1,57 @@ +"""Tests for the Modern Forms sensor platform.""" +from datetime import datetime + +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, DEVICE_CLASS_TIMESTAMP +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration, modern_forms_timers_set_mock +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms sensors.""" + + # await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock) + er.async_get(hass) + + # Light timer remaining time + state = hass.states.get("sensor.modernformsfan_light_sleep_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.state == "unknown" + + # Fan timer remaining time + state = hass.states.get("sensor.modernformsfan_fan_sleep_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.state == "unknown" + + +async def test_active_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms sensors.""" + + # await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock, mock_type=modern_forms_timers_set_mock) + er.async_get(hass) + + # Light timer remaining time + state = hass.states.get("sensor.modernformsfan_light_sleep_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + datetime.fromisoformat(state.state) + + # Fan timer remaining time + state = hass.states.get("sensor.modernformsfan_fan_sleep_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + datetime.fromisoformat(state.state) diff --git a/tests/fixtures/modern_forms/device_status_timers_active.json b/tests/fixtures/modern_forms/device_status_timers_active.json new file mode 100644 index 00000000000..e788b3e5882 --- /dev/null +++ b/tests/fixtures/modern_forms/device_status_timers_active.json @@ -0,0 +1,17 @@ +{ + "adaptiveLearning": false, + "awayModeEnabled": false, + "clientId": "MF_000000000000", + "decommission": false, + "factoryReset": false, + "fanDirection": "forward", + "fanOn": true, + "fanSleepTimer": 9999999999, + "fanSpeed": 3, + "lightBrightness": 50, + "lightOn": true, + "lightSleepTimer": 9999999999, + "resetRfPairList": false, + "rfPairModeActive": false, + "schedule": "" +} From 5e18b5c18951cf00291de7b7ed34baf14636e4da Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Jun 2021 03:09:38 -0500 Subject: [PATCH 630/750] Fix bug in detecting RainMachine zone soil type (#52273) * Fix bug in detecting RainMachine zone soil type * Simplify --- homeassistant/components/rainmachine/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index a90091c6c3a..fba8462d724 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -386,7 +386,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"), ATTR_RESTRICTIONS: self._data.get("restriction"), ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")), - ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("sun")), + ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("soil")), ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")), ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")), ATTR_TIME_REMAINING: self._data.get("remaining"), From 04300464da2cc094a30911b91a409956e15e482d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Jun 2021 03:20:11 -0500 Subject: [PATCH 631/750] Update RainMachine sprinkler and vegetation types (#52274) --- homeassistant/components/rainmachine/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index fba8462d724..d2dab8bd769 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -87,6 +87,7 @@ SPRINKLER_TYPE_MAP = { 2: "Rotors", 3: "Surface Drip", 4: "Bubblers Drip", + 5: "Rotors High Rate", 99: "Other", } @@ -94,6 +95,7 @@ SUN_EXPOSURE_MAP = {0: "Not Set", 1: "Full Sun", 2: "Partial Shade", 3: "Full Sh VEGETATION_MAP = { 0: "Not Set", + 1: "Not Set", 2: "Cool Season Grass", 3: "Fruit Trees", 4: "Flowers", From dee3e14df20bb67aa629501ee527e7be34523546 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Jun 2021 03:21:33 -0500 Subject: [PATCH 632/750] Fix values of RainMachine Freeze Protection and Hot Days binary sensors (#52275) * Fix values of RainMachine Freeze Protection and Hot Days binary sensors * Correct place * Fix --- .../components/rainmachine/binary_sensor.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 4b89b52befe..171fd26b910 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -179,18 +179,8 @@ class ProvisionSettingsBinarySensor(RainMachineBinarySensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FREEZE: - self._state = self.coordinator.data["freeze"] - elif self._sensor_type == TYPE_HOURLY: - self._state = self.coordinator.data["hourly"] - elif self._sensor_type == TYPE_MONTH: - self._state = self.coordinator.data["month"] - elif self._sensor_type == TYPE_RAINDELAY: - self._state = self.coordinator.data["rainDelay"] - elif self._sensor_type == TYPE_RAINSENSOR: - self._state = self.coordinator.data["rainSensor"] - elif self._sensor_type == TYPE_WEEKDAY: - self._state = self.coordinator.data["weekDay"] + if self._sensor_type == TYPE_FLOW_SENSOR: + self._state = self.coordinator.data["system"].get("useFlowSensor") class UniversalRestrictionsBinarySensor(RainMachineBinarySensor): @@ -199,5 +189,7 @@ class UniversalRestrictionsBinarySensor(RainMachineBinarySensor): @callback def update_from_latest_data(self) -> None: """Update the state.""" - if self._sensor_type == TYPE_FLOW_SENSOR: - self._state = self.coordinator.data["system"].get("useFlowSensor") + if self._sensor_type == TYPE_FREEZE_PROTECTION: + self._state = self.coordinator.data["freezeProtectEnabled"] + elif self._sensor_type == TYPE_HOT_DAYS: + self._state = self.coordinator.data["hotDaysExtraWatering"] From 91b4f7d1d53f9614b9a9287696a3346fd2d91d15 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 10:32:23 +0200 Subject: [PATCH 633/750] Filter MQTT alarm JSON attributes (#52278) --- .../components/mqtt/alarm_control_panel.py | 10 ++++++++++ tests/components/mqtt/test_alarm_control_panel.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 1e7ccf5bb4c..aa98a48dc10 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -55,6 +55,14 @@ CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night" CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" CONF_COMMAND_TEMPLATE = "command_template" +MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset( + { + alarm.ATTR_CHANGED_BY, + alarm.ATTR_CODE_ARM_REQUIRED, + alarm.ATTR_CODE_FORMAT, + } +) + DEFAULT_COMMAND_TEMPLATE = "{{action}}" DEFAULT_ARM_NIGHT = "ARM_NIGHT" DEFAULT_ARM_AWAY = "ARM_AWAY" @@ -112,6 +120,8 @@ async def _async_setup_entity( class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" + _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" self._state = None diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index cfb59ba27a7..c9d06ef343e 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -6,6 +6,9 @@ from unittest.mock import patch import pytest from homeassistant.components import alarm_control_panel +from homeassistant.components.mqtt.alarm_control_panel import ( + MQTT_ALARM_ATTRIBUTES_BLOCKED, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -39,6 +42,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -544,6 +548,17 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + MQTT_ALARM_ATTRIBUTES_BLOCKED, + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 040c88f982a04350332807c809104d55b0e28560 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:13:58 +0200 Subject: [PATCH 634/750] Filter MQTT climate JSON attributes (#52280) --- homeassistant/components/mqtt/climate.py | 27 ++++++++++++++++++++++++ tests/components/mqtt/test_climate.py | 9 ++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index da0ed485b72..ccaa7c65176 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -119,6 +119,31 @@ CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" CONF_TEMP_STEP = "temp_step" +MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( + { + climate.ATTR_AUX_HEAT, + climate.ATTR_CURRENT_HUMIDITY, + climate.ATTR_CURRENT_TEMPERATURE, + climate.ATTR_FAN_MODE, + climate.ATTR_FAN_MODES, + climate.ATTR_HUMIDITY, + climate.ATTR_HVAC_ACTION, + climate.ATTR_HVAC_MODES, + climate.ATTR_MAX_HUMIDITY, + climate.ATTR_MAX_TEMP, + climate.ATTR_MIN_HUMIDITY, + climate.ATTR_MIN_TEMP, + climate.ATTR_PRESET_MODE, + climate.ATTR_PRESET_MODES, + climate.ATTR_SWING_MODE, + climate.ATTR_SWING_MODES, + climate.ATTR_TARGET_TEMP_HIGH, + climate.ATTR_TARGET_TEMP_LOW, + climate.ATTR_TARGET_TEMP_STEP, + climate.ATTR_TEMPERATURE, + } +) + VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, CONF_AWAY_MODE_STATE_TEMPLATE, @@ -276,6 +301,8 @@ async def _async_setup_entity( class MqttClimate(MqttEntity, ClimateEntity): """Representation of an MQTT climate device.""" + _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the climate device.""" self._action = None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 546c112b153..24c9e4a5b74 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -23,6 +23,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED from homeassistant.const import STATE_OFF from homeassistant.setup import async_setup_component @@ -45,6 +46,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -923,6 +925,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 7de3e7d1dd6a88f324b125ad63976eee1feb38cf Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 29 Jun 2021 10:14:28 +0100 Subject: [PATCH 635/750] Support setting hvac_mode and temp in same homekit_controller set_temperature service call (#52195) * Support setting hvac_mode and temp in same set_temperature service call * Update homeassistant/components/homekit_controller/climate.py Co-authored-by: Paulus Schoutsen * Update homeassistant/components/homekit_controller/climate.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/homekit_controller/climate.py | 24 ++++++++++---- .../homekit_controller/test_climate.py | 31 ++++++++++++++++++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2c251d41fb3..ec0f383356e 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -19,6 +19,7 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, @@ -342,16 +343,27 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" + chars = {} + + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + mode = MODE_HOMEKIT_TO_HASS.get(value) + + if kwargs.get(ATTR_HVAC_MODE, mode) != mode: + mode = kwargs[ATTR_HVAC_MODE] + chars[CharacteristicsTypes.HEATING_COOLING_TARGET] = MODE_HASS_TO_HOMEKIT[ + mode + ] + temp = kwargs.get(ATTR_TEMPERATURE) heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + + if (mode == HVAC_MODE_HEAT_COOL) and ( SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features ): if temp is None: temp = (cool_temp + heat_temp) / 2 - await self.async_put_characteristics( + chars.update( { CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: heat_temp, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: cool_temp, @@ -359,9 +371,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): } ) else: - await self.async_put_characteristics( - {CharacteristicsTypes.TEMPERATURE_TARGET: temp} - ) + chars[CharacteristicsTypes.TEMPERATURE_TARGET] = temp + + await self.async_put_characteristics(chars) async def async_set_humidity(self, humidity): """Set new target humidity.""" diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 52671703cca..07a5025ac88 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -271,7 +271,6 @@ async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcn SERVICE_SET_TEMPERATURE, { "entity_id": "climate.testdevice", - "hvac_mode": HVAC_MODE_HEAT_COOL, "temperature": 22, "target_temp_low": 20, "target_temp_high": 24, @@ -370,6 +369,35 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): ) assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "temperature": 22, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + + +async def test_climate_set_mode_via_temp(hass, utcnow): + """Test setting temperature and mode at same tims.""" + helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "temperature": 21, + "hvac_mode": HVAC_MODE_HEAT, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + assert helper.characteristics[HEATING_COOLING_TARGET].value == 1 + await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -381,6 +409,7 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): blocking=True, ) assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + assert helper.characteristics[HEATING_COOLING_TARGET].value == 3 async def test_climate_change_thermostat_humidity(hass, utcnow): From 3b89fcfe831ddf2b58da8329ce7d61ef39af8fd6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:16:07 +0200 Subject: [PATCH 636/750] Filter MQTT lock JSON attributes (#52285) --- homeassistant/components/mqtt/lock.py | 9 +++++++++ tests/components/mqtt/test_lock.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index a48d271c196..45703fc0cc3 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -37,6 +37,13 @@ DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_STATE_LOCKED = "LOCKED" DEFAULT_STATE_UNLOCKED = "UNLOCKED" +MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( + { + lock.ATTR_CHANGED_BY, + lock.ATTR_CODE_FORMAT, + } +) + PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -76,6 +83,8 @@ async def _async_setup_entity( class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" + _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the lock.""" self._state = False diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 754f60f49b2..39250b0a2fa 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -10,6 +10,7 @@ from homeassistant.components.lock import ( STATE_LOCKED, STATE_UNLOCKED, ) +from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -32,6 +33,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -311,6 +313,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 71a0e474cc4630ea8905e3c604d3c12717a17c1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:16:57 +0200 Subject: [PATCH 637/750] Filter MQTT number JSON attributes (#52286) --- homeassistant/components/mqtt/number.py | 10 ++++++++++ tests/components/mqtt/test_number.py | 14 +++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index f7253541b69..866f93fd674 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -40,6 +40,14 @@ CONF_STEP = "step" DEFAULT_NAME = "MQTT Number" DEFAULT_OPTIMISTIC = False +MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( + { + number.ATTR_MAX, + number.ATTR_MIN, + number.ATTR_STEP, + } +) + def validate_config(config): """Validate that the configuration is valid, throws if it isn't.""" @@ -92,6 +100,8 @@ async def _async_setup_entity( class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """representation of an MQTT number.""" + _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT Number.""" self._config = config diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 8b62d1f9f33..37693340308 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest from homeassistant.components import number -from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN +from homeassistant.components.mqtt.number import ( + CONF_MAX, + CONF_MIN, + MQTT_NUMBER_ATTRIBUTES_BLOCKED, +) from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, @@ -37,6 +41,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -250,6 +255,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From e0f79875449e58a749dd46e00c38af2fae050a19 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:18:50 +0200 Subject: [PATCH 638/750] Filter MQTT fan JSON attributes (#52283) --- homeassistant/components/mqtt/fan.py | 14 ++++++++++++++ tests/components/mqtt/test_fan.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index d92b6dfd21f..ef996a3c4ba 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -94,6 +94,18 @@ DEFAULT_SPEED_RANGE_MAX = 100 OSCILLATE_ON_PAYLOAD = "oscillate_on" OSCILLATE_OFF_PAYLOAD = "oscillate_off" +MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset( + { + fan.ATTR_DIRECTION, + fan.ATTR_OSCILLATING, + fan.ATTR_PERCENTAGE_STEP, + fan.ATTR_PERCENTAGE, + fan.ATTR_PRESET_MODE, + fan.ATTR_PRESET_MODES, + fan.ATTR_SPEED_LIST, + fan.ATTR_SPEED, + } +) _LOGGER = logging.getLogger(__name__) @@ -223,6 +235,8 @@ async def _async_setup_entity( class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" + _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" self._state = False diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index dad365e3c66..438ae0978c6 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -6,6 +6,7 @@ from voluptuous.error import MultipleInvalid from homeassistant.components import fan from homeassistant.components.fan import NotValidPresetModeError +from homeassistant.components.mqtt.fan import MQTT_FAN_ATTRIBUTES_BLOCKED from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -33,6 +34,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -2037,6 +2039,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 61f7f5c96af63035c7a0bf56a194330faec1e510 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:19:40 +0200 Subject: [PATCH 639/750] Filter MQTT sensor JSON attributes (#52289) --- homeassistant/components/mqtt/sensor.py | 8 ++++++++ tests/components/mqtt/test_sensor.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 51caeb5f6da..777a15b639a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -44,6 +44,13 @@ CONF_LAST_RESET_TOPIC = "last_reset_topic" CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template" CONF_STATE_CLASS = "state_class" +MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset( + { + sensor.ATTR_LAST_RESET, + sensor.ATTR_STATE_CLASS, + } +) + DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( @@ -87,6 +94,7 @@ class MqttSensor(MqttEntity, SensorEntity): """Representation of a sensor that can be updated using MQTT.""" _attr_last_reset = None + _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d90737a7b7f..15ca9870077 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest +from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.core as ha @@ -535,7 +536,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED ) From de4cfb0ce2481e80fc2d4f2c6ee3875f220b7554 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:21:22 +0200 Subject: [PATCH 640/750] Filter MQTT vacuum JSON attributes (#52291) --- homeassistant/components/mqtt/vacuum/const.py | 10 ++++++++++ .../components/mqtt/vacuum/schema_legacy.py | 8 ++++++++ .../components/mqtt/vacuum/schema_state.py | 3 +++ tests/components/mqtt/test_legacy_vacuum.py | 13 +++++++++++++ tests/components/mqtt/test_state_vacuum.py | 9 +++++++++ 5 files changed, 43 insertions(+) create mode 100644 homeassistant/components/mqtt/vacuum/const.py diff --git a/homeassistant/components/mqtt/vacuum/const.py b/homeassistant/components/mqtt/vacuum/const.py new file mode 100644 index 00000000000..26e11125556 --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/const.py @@ -0,0 +1,10 @@ +"""Shared constants.""" +from homeassistant.components import vacuum + +MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset( + { + vacuum.ATTR_BATTERY_ICON, + vacuum.ATTR_BATTERY_LEVEL, + vacuum.ATTR_FAN_SPEED, + } +) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 87044c46cfb..009cfe18016 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -4,6 +4,7 @@ import json import voluptuous as vol from homeassistant.components.vacuum import ( + ATTR_STATUS, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, @@ -26,6 +27,7 @@ from .. import subscription from ... import mqtt from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services SERVICE_TO_STRING = { @@ -96,6 +98,10 @@ DEFAULT_PAYLOAD_TURN_ON = "turn_on" DEFAULT_RETAIN = False DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES, SERVICE_TO_STRING) +MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozenset( + {ATTR_STATUS} +) + PLATFORM_SCHEMA_LEGACY = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { @@ -160,6 +166,8 @@ async def async_setup_entity_legacy( class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" + _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" self._cleaning = False diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index f35a036ec10..2c222af28d8 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -30,6 +30,7 @@ from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subs from ... import mqtt from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services SERVICE_TO_STRING = { @@ -144,6 +145,8 @@ async def async_setup_entity_state( class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" + _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" self._state = None diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index db25e66c2c2..2dc8993a565 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -11,6 +11,7 @@ from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( ALL_SERVICES, + MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, SERVICE_TO_STRING, ) from homeassistant.components.vacuum import ( @@ -42,6 +43,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -581,6 +583,17 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock, + vacuum.DOMAIN, + DEFAULT_CONFIG_2, + MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index e18b0b05835..46cc5552b6a 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components import vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum +from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING from homeassistant.components.vacuum import ( @@ -52,6 +53,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -359,6 +361,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 74e61ab7f75f228e87de16bf0b8b6ca03e4accdb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:22:25 +0200 Subject: [PATCH 641/750] Filter MQTT switch JSON attributes (#52290) --- homeassistant/components/mqtt/switch.py | 9 +++++++++ tests/components/mqtt/test_switch.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 5dfc26ce613..f383a2ae310 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -32,6 +32,13 @@ from .. import mqtt from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +MQTT_SWITCH_ATTRIBUTES_BLOCKED = frozenset( + { + switch.ATTR_CURRENT_POWER_W, + switch.ATTR_TODAY_ENERGY_KWH, + } +) + DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" @@ -78,6 +85,8 @@ async def _async_setup_entity( class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" + _attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" self._state = False diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 607e4468a5f..d5dcfbd4fa7 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import switch +from homeassistant.components.mqtt.switch import MQTT_SWITCH_ATTRIBUTES_BLOCKED from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -29,6 +30,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -246,6 +248,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, MQTT_SWITCH_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From a7dd7c1a3d591241c38aa432ff786a3d268c1ad4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:22:58 +0200 Subject: [PATCH 642/750] Filter MQTT select JSON attributes (#52288) --- homeassistant/components/mqtt/select.py | 8 ++++++++ tests/components/mqtt/test_select.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 310b3e508f1..98643917788 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -33,6 +33,12 @@ CONF_OPTIONS = "options" DEFAULT_NAME = "MQTT Select" DEFAULT_OPTIMISTIC = False +MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( + { + select.ATTR_OPTIONS, + } +) + def validate_config(config): """Validate that the configuration is valid, throws if it isn't.""" @@ -81,6 +87,8 @@ async def _async_setup_entity( class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" + _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT select.""" self._config = config diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 41fa302a6b9..5dad989a5cf 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -5,7 +5,10 @@ from unittest.mock import patch import pytest from homeassistant.components import select -from homeassistant.components.mqtt.select import CONF_OPTIONS +from homeassistant.components.mqtt.select import ( + CONF_OPTIONS, + MQTT_SELECT_ATTRIBUTES_BLOCKED, +) from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, @@ -35,6 +38,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -230,6 +234,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From 8a825571423538258bc45d0cd21f30a21826143e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 11:32:46 +0200 Subject: [PATCH 643/750] Demo: Remote improvements (#52265) * Demo: Remote improvements * Address pylint warning --- homeassistant/components/demo/remote.py | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 98e949f38c3..1badd391575 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -1,14 +1,32 @@ """Demo platform that has two fake remotes.""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + from homeassistant.components.remote import RemoteEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Demo config entry.""" setup_platform(hass, {}, async_add_entities) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities_callback: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the demo remotes.""" add_entities_callback( [ @@ -21,50 +39,32 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): class DemoRemote(RemoteEntity): """Representation of a demo remote.""" - def __init__(self, name, state, icon): + _attr_should_poll = False + + def __init__(self, name: str | None, state: bool, icon: str | None) -> None: """Initialize the Demo Remote.""" - self._name = name or DEVICE_DEFAULT_NAME - self._state = state - self._icon = icon + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_is_on = state + self._attr_icon = icon self._last_command_sent = None @property - def should_poll(self): - """No polling needed for a demo remote.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def is_on(self): - """Return true if remote is on.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return device state attributes.""" if self._last_command_sent is not None: return {"last_command_sent": self._last_command_sent} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the remote on.""" - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the remote off.""" - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() - def send_command(self, command, **kwargs): + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to a device.""" for com in command: self._last_command_sent = com From 30a9198d978cace81e5ec01587447e5890f459b5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:33:26 +0200 Subject: [PATCH 644/750] Add test to MQTT device tracker (#52292) --- tests/components/mqtt/test_common.py | 7 ++----- .../mqtt/test_device_tracker_discovery.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index d8a263131a9..c3a4022fdd8 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -506,11 +506,8 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( # Add JSON attributes settings to config config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" - assert await async_setup_component( - hass, - domain, - config, - ) + data = json.dumps(config[domain]) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() val = "abc123" diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 174db8f017a..4020c2beaeb 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -2,11 +2,22 @@ import pytest +from homeassistant.components import device_tracker from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN +from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message + from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry +DEFAULT_CONFIG = { + device_tracker.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + } +} + @pytest.fixture def device_reg(hass): @@ -360,3 +371,10 @@ async def test_setting_device_tracker_location_via_lat_lon_message( state = hass.states.get("device_tracker.test") assert state.attributes["latitude"] == 32.87336 assert state.state == STATE_UNKNOWN + + +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, device_tracker.DOMAIN, DEFAULT_CONFIG, None + ) From 39a064683ae4b96adba3787036da2cc65d392dd2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 11:59:23 +0200 Subject: [PATCH 645/750] Filter MQTT cover JSON attributes (#52282) --- homeassistant/components/mqtt/cover.py | 9 +++++++++ tests/components/mqtt/test_cover.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 0a78a102a13..fd3e36c04e1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -101,6 +101,13 @@ TILT_FEATURES = ( | SUPPORT_SET_TILT_POSITION ) +MQTT_COVER_ATTRIBUTES_BLOCKED = frozenset( + { + cover.ATTR_CURRENT_POSITION, + cover.ATTR_CURRENT_TILT_POSITION, + } +) + def validate_options(value): """Validate options. @@ -219,6 +226,8 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" + _attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the cover.""" self._position = None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c5a8552dc20..7d763895428 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -20,6 +20,7 @@ from homeassistant.components.mqtt.cover import ( CONF_TILT_COMMAND_TOPIC, CONF_TILT_STATUS_TEMPLATE, CONF_TILT_STATUS_TOPIC, + MQTT_COVER_ATTRIBUTES_BLOCKED, MqttCover, ) from homeassistant.const import ( @@ -62,6 +63,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -2307,6 +2309,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From b77f2b9e12d21d5edb8434df52a40f29d3bf92a7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 12:14:02 +0200 Subject: [PATCH 646/750] Filter MQTT camera JSON attributes (#52279) * Filter MQTT camera JSON attributes * Add missing attribute to blocked list --- homeassistant/components/mqtt/camera.py | 11 +++++++++++ tests/components/mqtt/test_camera.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 1db9faf9058..adcb9ca623a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -19,6 +19,15 @@ from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_hel CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Camera" +MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( + { + "access_token", + "brand", + "model_name", + "motion_detection", + } +) + PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -53,6 +62,8 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" + _attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT Camera.""" self._last_image = None diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 13c15796cfd..a73a63d3439 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components import camera +from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED from homeassistant.setup import async_setup_component from .test_common import ( @@ -26,6 +27,7 @@ from .test_common import ( help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -94,6 +96,13 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): ) +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED + ) + + async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( From c785db4ffad3600b61df58f72a4aed4a80585ec2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 12:20:10 +0200 Subject: [PATCH 647/750] Normalize energy statistics to kWh (#52238) --- homeassistant/components/sensor/recorder.py | 53 +++++++++++++++++++-- tests/components/sensor/test_recorder.py | 36 ++++++++++---- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 6de5ee6338a..35039de93c4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime import itertools +import logging from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( @@ -15,12 +16,19 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, ) -from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, +) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util from . import DOMAIN +_LOGGER = logging.getLogger(__name__) + DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_ENERGY: {"sum"}, @@ -30,6 +38,15 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_MONETARY: {"sum"}, } +UNIT_CONVERSIONS = { + DEVICE_CLASS_ENERGY: { + ENERGY_KILO_WATT_HOUR: lambda x: x, + ENERGY_WATT_HOUR: lambda x: x / 1000, + } +} + +WARN_UNSUPPORTED_UNIT = set() + def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]: """Get (entity_id, device_class) of all sensors for which to compile statistics.""" @@ -92,6 +109,36 @@ def _time_weighted_average( return accumulated / (end - start).total_seconds() +def _normalize_states( + entity_history: list[State], device_class: str, entity_id: str +) -> list[tuple[float, State]]: + """Normalize units.""" + + if device_class not in UNIT_CONVERSIONS: + # We're not normalizing this device class, return the state as they are + return [(float(el.state), el) for el in entity_history if _is_number(el.state)] + + fstates = [] + + for state in entity_history: + # Exclude non numerical states from statistics + if not _is_number(state.state): + continue + + fstate = float(state.state) + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + # Exclude unsupported units from statistics + if unit not in UNIT_CONVERSIONS[device_class]: + if entity_id not in WARN_UNSUPPORTED_UNIT: + WARN_UNSUPPORTED_UNIT.add(entity_id) + _LOGGER.warning("%s has unknown unit %s", entity_id, unit) + continue + + fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) # type: ignore + + return fstates + + def compile_statistics( hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime ) -> dict: @@ -115,9 +162,7 @@ def compile_statistics( continue entity_history = history_list[entity_id] - fstates = [ - (float(el.state), el) for el in entity_history if _is_number(el.state) - ] + fstates = _normalize_states(entity_history, device_class, entity_id) if not fstates: continue diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 47a950f9eaa..b8411e69a7f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -49,7 +49,11 @@ def test_compile_hourly_energy_statistics(hass_recorder): hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = {"device_class": "energy", "state_class": "measurement"} + sns1_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + } sns2_attr = {"device_class": "energy"} sns3_attr = {} @@ -109,9 +113,21 @@ def test_compile_hourly_energy_statistics2(hass_recorder): hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = {"device_class": "energy", "state_class": "measurement"} - sns2_attr = {"device_class": "energy", "state_class": "measurement"} - sns3_attr = {"device_class": "energy", "state_class": "measurement"} + sns1_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + } + sns2_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + } + sns3_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "Wh", + } zero, four, eight, states = record_energy_states( hass, sns1_attr, sns2_attr, sns3_attr @@ -201,8 +217,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(zero), - "state": approx(5.0), - "sum": approx(5.0), + "state": approx(5.0 / 1000), + "sum": approx(5.0 / 1000), }, { "statistic_id": "sensor.test3", @@ -211,8 +227,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(50.0), - "sum": approx(30.0), + "state": approx(50.0 / 1000), + "sum": approx(30.0 / 1000), }, { "statistic_id": "sensor.test3", @@ -221,8 +237,8 @@ def test_compile_hourly_energy_statistics2(hass_recorder): "mean": None, "min": None, "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(90.0), - "sum": approx(70.0), + "state": approx(90.0 / 1000), + "sum": approx(70.0 / 1000), }, ], } From 3311b1bafb28257b53f3765f959556272c56813a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 13:08:19 +0200 Subject: [PATCH 648/750] Small clean up for Motion Blinds (#52281) --- .../components/motion_blinds/__init__.py | 12 +-- .../components/motion_blinds/cover.py | 46 +++--------- .../components/motion_blinds/sensor.py | 74 ++++--------------- 3 files changed, 25 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index d2400beb4f5..f7ae6573b1b 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -80,12 +80,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): """Fetch the latest data from the gateway and blinds.""" data = await self.hass.async_add_executor_job(self.update_gateway) - all_available = True - for device in data.values(): - if not device[ATTR_AVAILABLE]: - all_available = False - break - + all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: self.update_interval = timedelta(seconds=UPDATE_INTERVAL) else: @@ -94,11 +89,6 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): return data -def setup(hass: core.HomeAssistant, config: dict): - """Set up the Motion Blinds component.""" - return True - - async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index a802ecfb667..60ec375a9c0 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -132,32 +132,19 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): super().__init__(coordinator) self._blind = blind - self._device_class = device_class self._config_entry = config_entry - @property - def unique_id(self): - """Return the unique id of the blind.""" - return self._blind.mac - - @property - def device_info(self): - """Return the device info of the blind.""" - device_info = { - "identifiers": {(DOMAIN, self._blind.mac)}, + self._attr_device_class = device_class + self._attr_name = f"{blind.blind_type}-{blind.mac[12:]}" + self._attr_unique_id = blind.mac + self._attr_device_info = { + "identifiers": {(DOMAIN, blind.mac)}, "manufacturer": MANUFACTURER, - "name": f"{self._blind.blind_type}-{self._blind.mac[12:]}", - "model": self._blind.blind_type, - "via_device": (DOMAIN, self._config_entry.unique_id), + "name": f"{blind.blind_type}-{blind.mac[12:]}", + "model": blind.blind_type, + "via_device": (DOMAIN, config_entry.unique_id), } - return device_info - - @property - def name(self): - """Return the name of the blind.""" - return f"{self._blind.blind_type}-{self._blind.mac[12:]}" - @property def available(self): """Return True if entity is available.""" @@ -180,11 +167,6 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): return None return 100 - self._blind.position - @property - def device_class(self): - """Return the device class.""" - return self._device_class - @property def is_closed(self): """Return if the cover is closed or not.""" @@ -263,20 +245,12 @@ class MotionTDBUDevice(MotionPositionDevice): super().__init__(coordinator, blind, device_class, config_entry) self._motor = motor self._motor_key = motor[0] + self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}" + self._attr_unique_id = f"{blind.mac}-{motor}" if self._motor not in ["Bottom", "Top", "Combined"]: _LOGGER.error("Unknown motor '%s'", self._motor) - @property - def unique_id(self): - """Return the unique id of the blind.""" - return f"{self._blind.mac}-{self._motor}" - - @property - def name(self): - """Return the name of the blind.""" - return f"{self._blind.blind_type}-{self._motor}-{self._blind.mac[12:]}" - @property def current_cover_position(self): """ diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 0da38795f7b..be88a099f25 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -46,26 +46,17 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): Updates are done by the cover platform. """ + _attr_device_class = DEVICE_CLASS_BATTERY + _attr_unit_of_measurement = PERCENTAGE + def __init__(self, coordinator, blind): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator) self._blind = blind - - @property - def unique_id(self): - """Return the unique id of the blind.""" - return f"{self._blind.mac}-battery" - - @property - def device_info(self): - """Return the device info of the blind.""" - return {"identifiers": {(DOMAIN, self._blind.mac)}} - - @property - def name(self): - """Return the name of the blind battery sensor.""" - return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}" + self._attr_device_info = {"identifiers": {(DOMAIN, blind.mac)}} + self._attr_name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + self._attr_unique_id = f"{blind.mac}-battery" @property def available(self): @@ -78,16 +69,6 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return PERCENTAGE - - @property - def device_class(self): - """Return the device class of this entity.""" - return DEVICE_CLASS_BATTERY - @property def state(self): """Return the state of the sensor.""" @@ -121,16 +102,8 @@ class MotionTDBUBatterySensor(MotionBatterySensor): super().__init__(coordinator, blind) self._motor = motor - - @property - def unique_id(self): - """Return the unique id of the blind.""" - return f"{self._blind.mac}-{self._motor}-battery" - - @property - def name(self): - """Return the name of the blind battery sensor.""" - return f"{self._blind.blind_type}-{self._motor}-battery-{self._blind.mac[12:]}" + self._attr_unique_id = f"{blind.mac}-{motor}-battery" + self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" @property def state(self): @@ -153,22 +126,18 @@ class MotionTDBUBatterySensor(MotionBatterySensor): class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Representation of a Motion Signal Strength Sensor.""" + _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_entity_registry_enabled_default = False + _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + def __init__(self, coordinator, device, device_type): """Initialize the Motion Signal Strength Sensor.""" super().__init__(coordinator) self._device = device self._device_type = device_type - - @property - def unique_id(self): - """Return the unique id of the blind.""" - return f"{self._device.mac}-RSSI" - - @property - def device_info(self): - """Return the device info of the blind.""" - return {"identifiers": {(DOMAIN, self._device.mac)}} + self._attr_device_info = {"identifiers": {(DOMAIN, device.mac)}} + self._attr_unique_id = f"{device.mac}-RSSI" @property def name(self): @@ -192,21 +161,6 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): and self.coordinator.data[self._device.mac][ATTR_AVAILABLE] ) - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SIGNAL_STRENGTH_DECIBELS_MILLIWATT - - @property - def device_class(self): - """Return the device class of this entity.""" - return DEVICE_CLASS_SIGNAL_STRENGTH - - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return False - @property def state(self): """Return the state of the sensor.""" From a639cb7ba787458c56420777845f627d121a2947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Moreno?= Date: Tue, 29 Jun 2021 13:52:39 +0200 Subject: [PATCH 649/750] Add sensor platform to Meteoclimatic integration (#51467) * Add meteoclimatic sensor platform Signed-off-by: Adrian Moreno * Add sensor.py to coverage file Signed-off-by: Adrian Moreno * Add explicit return type None Signed-off-by: Adrian Moreno * Fix sample station code Signed-off-by: Adrian Moreno * Apply frenck suggestions Signed-off-by: Adrian Moreno * Remove extra attributes Signed-off-by: Adrian Moreno * Revert translations Signed-off-by: Adrian Moreno * Remove None icons and classes Signed-off-by: Adrian Moreno --- .coveragerc | 1 + .../components/meteoclimatic/__init__.py | 2 +- .../components/meteoclimatic/const.py | 22 +++--- .../components/meteoclimatic/sensor.py | 79 +++++++++++++++++++ .../components/meteoclimatic/weather.py | 13 ++- 5 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/meteoclimatic/sensor.py diff --git a/.coveragerc b/.coveragerc index 29ba3dcdeb1..ade61c1fb07 100644 --- a/.coveragerc +++ b/.coveragerc @@ -611,6 +611,7 @@ omit = homeassistant/components/meteoalarm/* homeassistant/components/meteoclimatic/__init__.py homeassistant/components/meteoclimatic/const.py + homeassistant/components/meteoclimatic/sensor.py homeassistant/components/meteoclimatic/weather.py homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 20f72fd4410..58e51d0490a 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = DataUpdateCoordinator( hass, _LOGGER, - name=f"Meteoclimatic Coordinator for {station_code}", + name=f"Meteoclimatic weather for {entry.title} ({station_code})", update_method=async_update_data, update_interval=SCAN_INTERVAL, ) diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py index eb3823a9b42..cd4be5821ea 100644 --- a/homeassistant/components/meteoclimatic/const.py +++ b/homeassistant/components/meteoclimatic/const.py @@ -34,8 +34,10 @@ from homeassistant.const import ( ) DOMAIN = "meteoclimatic" -PLATFORMS = ["weather"] +PLATFORMS = ["sensor", "weather"] ATTRIBUTION = "Data provided by Meteoclimatic" +MODEL = "Meteoclimatic RSS feed" +MANUFACTURER = "Meteoclimatic" SCAN_INTERVAL = timedelta(minutes=10) @@ -54,12 +56,12 @@ SENSOR_TYPES = { SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, }, "temp_max": { - SENSOR_TYPE_NAME: "Max Temp.", + SENSOR_TYPE_NAME: "Daily Max Temperature", SENSOR_TYPE_UNIT: TEMP_CELSIUS, SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, }, "temp_min": { - SENSOR_TYPE_NAME: "Min Temp.", + SENSOR_TYPE_NAME: "Daily Min Temperature", SENSOR_TYPE_UNIT: TEMP_CELSIUS, SENSOR_TYPE_CLASS: DEVICE_CLASS_TEMPERATURE, }, @@ -69,12 +71,12 @@ SENSOR_TYPES = { SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, }, "humidity_max": { - SENSOR_TYPE_NAME: "Max Humidity", + SENSOR_TYPE_NAME: "Daily Max Humidity", SENSOR_TYPE_UNIT: PERCENTAGE, SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, }, "humidity_min": { - SENSOR_TYPE_NAME: "Min Humidity", + SENSOR_TYPE_NAME: "Daily Min Humidity", SENSOR_TYPE_UNIT: PERCENTAGE, SENSOR_TYPE_CLASS: DEVICE_CLASS_HUMIDITY, }, @@ -84,12 +86,12 @@ SENSOR_TYPES = { SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, }, "pressure_max": { - SENSOR_TYPE_NAME: "Max Pressure", + SENSOR_TYPE_NAME: "Daily Max Pressure", SENSOR_TYPE_UNIT: PRESSURE_HPA, SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, }, "pressure_min": { - SENSOR_TYPE_NAME: "Min Pressure", + SENSOR_TYPE_NAME: "Daily Min Pressure", SENSOR_TYPE_UNIT: PRESSURE_HPA, SENSOR_TYPE_CLASS: DEVICE_CLASS_PRESSURE, }, @@ -99,7 +101,7 @@ SENSOR_TYPES = { SENSOR_TYPE_ICON: "mdi:weather-windy", }, "wind_max": { - SENSOR_TYPE_NAME: "Max Wind Speed", + SENSOR_TYPE_NAME: "Daily Max Wind Speed", SENSOR_TYPE_UNIT: SPEED_KILOMETERS_PER_HOUR, SENSOR_TYPE_ICON: "mdi:weather-windy", }, @@ -109,9 +111,9 @@ SENSOR_TYPES = { SENSOR_TYPE_ICON: "mdi:weather-windy", }, "rain": { - SENSOR_TYPE_NAME: "Rain", + SENSOR_TYPE_NAME: "Daily Precipitation", SENSOR_TYPE_UNIT: LENGTH_MILLIMETERS, - SENSOR_TYPE_ICON: "mdi:weather-rainy", + SENSOR_TYPE_ICON: "mdi:cup-water", }, } diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py new file mode 100644 index 00000000000..bcd597d4b0c --- /dev/null +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -0,0 +1,79 @@ +"""Support for Meteoclimatic sensor.""" +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + ATTRIBUTION, + DOMAIN, + MANUFACTURER, + MODEL, + SENSOR_TYPE_CLASS, + SENSOR_TYPE_ICON, + SENSOR_TYPE_NAME, + SENSOR_TYPE_UNIT, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Meteoclimatic sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [MeteoclimaticSensor(sensor_type, coordinator) for sensor_type in SENSOR_TYPES], + False, + ) + + +class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): + """Representation of a Meteoclimatic sensor.""" + + def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None: + """Initialize the Meteoclimatic sensor.""" + super().__init__(coordinator) + self._type = sensor_type + station = self.coordinator.data["station"] + self._attr_device_class = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_CLASS) + self._attr_icon = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_ICON) + self._attr_name = ( + f"{station.name} {SENSOR_TYPES[sensor_type][SENSOR_TYPE_NAME]}" + ) + self._attr_unique_id = f"{station.code}_{sensor_type}" + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type].get(SENSOR_TYPE_UNIT) + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + + @property + def state(self): + """Return the state of the sensor.""" + return ( + getattr(self.coordinator.data["weather"], self._type) + if self.coordinator.data + else None + ) + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 98507eae995..1326d700826 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN +from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN, MANUFACTURER, MODEL def format_condition(condition): @@ -52,6 +52,17 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Return the unique id of the sensor.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + @property def condition(self): """Return the current condition.""" From e1797ea6709e4489ce592aa04e89927e3a9b6cec Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 30 Jun 2021 00:33:04 +1200 Subject: [PATCH 650/750] Add number entities to ESPHome (#52241) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/esphome/entry_data.py | 2 + .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/number.py | 85 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/number.py diff --git a/.coveragerc b/.coveragerc index ade61c1fb07..6fb95b595cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -274,6 +274,7 @@ omit = homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py + homeassistant/components/esphome/number.py homeassistant/components/esphome/sensor.py homeassistant/components/esphome/switch.py homeassistant/components/essent/sensor.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index dc964d0eb2e..49655de8fb7 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -16,6 +16,7 @@ from aioesphomeapi import ( EntityState, FanInfo, LightInfo, + NumberInfo, SensorInfo, SwitchInfo, TextSensorInfo, @@ -41,6 +42,7 @@ INFO_TYPE_TO_PLATFORM = { CoverInfo: "cover", FanInfo: "fan", LightInfo: "light", + NumberInfo: "number", SensorInfo: "sensor", SwitchInfo: "switch", TextSensorInfo: "sensor", diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 9182be8d496..284fdc25956 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==3.0.1"], + "requirements": ["aioesphomeapi==3.1.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py new file mode 100644 index 00000000000..08b31e91b79 --- /dev/null +++ b/homeassistant/components/esphome/number.py @@ -0,0 +1,85 @@ +"""Support for esphome numbers.""" +from __future__ import annotations + +import math + +from aioesphomeapi import NumberInfo, NumberState +import voluptuous as vol + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry + +ICON_SCHEMA = vol.Schema(cv.icon) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome numbers based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="number", + info_type=NumberInfo, + entity_type=EsphomeNumber, + state_type=NumberState, + ) + + +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + +class EsphomeNumber(EsphomeEntity, NumberEntity): + """A number implementation for esphome.""" + + @property + def _static_info(self) -> NumberInfo: + return super()._static_info + + @property + def _state(self) -> NumberState | None: + return super()._state + + @property + def icon(self) -> str | None: + """Return the icon.""" + if not self._static_info.icon: + return None + return ICON_SCHEMA(self._static_info.icon) + + @property + def min_value(self) -> float: + """Return the minimum value.""" + return super()._static_info.min_value + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return super()._static_info.max_value + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + return super()._static_info.step + + @esphome_state_property + def value(self) -> float: + """Return the state of the entity.""" + if math.isnan(self._state.state): + return None + if self._state.missing_state: + return None + return self._state.state + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + await self._client.number_command(self._static_info.key, value) diff --git a/requirements_all.txt b/requirements_all.txt index 3f6d3e5ccec..eb3e6115da7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==3.0.1 +aioesphomeapi==3.1.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dfec7520a5..4b019058c77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==3.0.1 +aioesphomeapi==3.1.0 # homeassistant.components.flo aioflo==0.4.1 From 6131ed09f0e4b31cba832d4c4099c82ab5e985f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 14:48:08 +0200 Subject: [PATCH 651/750] Compile statistics for power sensors (#52299) --- homeassistant/components/sensor/recorder.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 35039de93c4..fc4cf9b74f2 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -19,8 +19,11 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_KILO_WATT, + POWER_WATT, ) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util @@ -33,16 +36,21 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_ENERGY: {"sum"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, + DEVICE_CLASS_MONETARY: {"sum"}, + DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, - DEVICE_CLASS_MONETARY: {"sum"}, } UNIT_CONVERSIONS = { DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, ENERGY_WATT_HOUR: lambda x: x / 1000, - } + }, + DEVICE_CLASS_POWER: { + POWER_WATT: lambda x: x, + POWER_KILO_WATT: lambda x: x * 1000, + }, } WARN_UNSUPPORTED_UNIT = set() From d3210ada1dc4b6cf36b1f1f3d79ba4b6028e4a59 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 15:07:31 +0200 Subject: [PATCH 652/750] Allow None value return type for Number entity state value (#52302) --- homeassistant/components/number/__init__.py | 4 ++-- homeassistant/components/zwave_js/number.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 4b1049e36a2..cbfdea7fa11 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -108,12 +108,12 @@ class NumberEntity(Entity): @property @final - def state(self) -> float: + def state(self) -> float | None: """Return the entity state.""" return self.value @property - def value(self) -> float: + def value(self) -> float | None: """Return the entity value to represent the entity state.""" return self._attr_value diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 808fd346be1..2a3c9820a69 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -71,7 +71,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): return float(self.info.primary_value.metadata.max) @property - def value(self) -> float | None: # type: ignore + def value(self) -> float | None: """Return the entity value.""" if self.info.primary_value.value is None: return None From 720a67957b562368add8a3902fb679bc975d0b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 29 Jun 2021 16:04:21 +0200 Subject: [PATCH 653/750] Bump hass-nabucasa to 0.44.0 (#52303) --- 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 d0d7ae09505..7516f32c3e1 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.43.0"], + "requirements": ["hass-nabucasa==0.44.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a5a2288f202..9da1bc22f3e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==3.3.2 defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 -hass-nabucasa==0.43.0 +hass-nabucasa==0.44.0 home-assistant-frontend==20210603.0 httpx==0.18.0 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index eb3e6115da7..b95c9a26ae6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.43.0 +hass-nabucasa==0.44.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b019058c77..933122e1c49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ habitipy==0.2.0 hangups==0.4.14 # homeassistant.components.cloud -hass-nabucasa==0.43.0 +hass-nabucasa==0.44.0 # homeassistant.components.tasmota hatasmota==0.2.19 From f1b40b683d612bafb702aa4c6d91a5185b6092e3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 16:09:42 +0200 Subject: [PATCH 654/750] Disable dependency checks and tests for disabled EE Brightbox integration (#52304) --- homeassistant/components/ee_brightbox/device_tracker.py | 1 + tests/components/ee_brightbox/test_device_tracker.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 64f81023796..f29eaf6f948 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -1,6 +1,7 @@ """Support for EE Brightbox router.""" import logging +# pylint: disable=import-error from eebrightbox import EEBrightBox, EEBrightBoxException import voluptuous as vol diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 06c3ce0cd1d..e88a481648f 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -9,6 +9,9 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM from homeassistant.setup import async_setup_component +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def _configure_mock_get_devices(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value From 8a00c3a2f5d435d4455ca0afa58c36d41dba42a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 16:31:29 +0200 Subject: [PATCH 655/750] Implement color_mode support for kulersky (#52080) --- homeassistant/components/kulersky/light.py | 67 ++++------- tests/components/kulersky/test_light.py | 132 ++++++++++++++------- 2 files changed, 106 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 48f27e91c79..fd907235b45 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -8,11 +8,8 @@ import pykulersky from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, + ATTR_RGBW_COLOR, + COLOR_MODE_RGBW, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -20,14 +17,11 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.color as color_util from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN _LOGGER = logging.getLogger(__name__) -SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE - DISCOVERY_INTERVAL = timedelta(seconds=60) @@ -71,10 +65,9 @@ class KulerskyLight(LightEntity): def __init__(self, light: pykulersky.Light) -> None: """Initialize a Kuler Sky light.""" self._light = light - self._hs_color = None - self._brightness = None - self._white_value = None self._available = None + self._attr_supported_color_modes = {COLOR_MODE_RGBW} + self._attr_color_mode = COLOR_MODE_RGBW async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @@ -112,30 +105,10 @@ class KulerskyLight(LightEntity): "manufacturer": "Brightech", } - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_KULERSKY - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs_color - - @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white_value - @property def is_on(self): """Return true if light is on.""" - return self._brightness > 0 or self._white_value > 0 + return self.brightness > 0 @property def available(self) -> bool: @@ -144,24 +117,21 @@ class KulerskyLight(LightEntity): async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" - default_hs = (0, 0) if self._hs_color is None else self._hs_color - hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) + default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color + rgbw = kwargs.get(ATTR_RGBW_COLOR, default_rgbw) - default_brightness = 0 if self._brightness is None else self._brightness + default_brightness = 0 if self.brightness is None else self.brightness brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) - default_white_value = 255 if self._white_value is None else self._white_value - white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value) - - if brightness == 0 and white_value == 0 and not kwargs: + if brightness == 0 and not kwargs: # If the light would be off, and no additional parameters were # passed, just turn the light on full brightness. brightness = 255 - white_value = 255 + rgbw = (255,) * 4 - rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + rgbw_scaled = [round(x * brightness / 255) for x in rgbw] - await self._light.set_color(*rgb, white_value) + await self._light.set_color(*rgbw_scaled) async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" @@ -173,7 +143,7 @@ class KulerskyLight(LightEntity): if not self._available: await self._light.connect() # pylint: disable=invalid-name - r, g, b, w = await self._light.get_color() + rgbw = await self._light.get_color() except pykulersky.PykulerskyException as exc: if self._available: _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) @@ -183,7 +153,10 @@ class KulerskyLight(LightEntity): _LOGGER.info("Reconnected to %s", self._light.address) self._available = True - hsv = color_util.color_RGB_to_hsv(r, g, b) - self._hs_color = hsv[:2] - self._brightness = int(round((hsv[2] / 100) * 255)) - self._white_value = w + brightness = max(rgbw) + if not brightness: + rgbw_normalized = [0, 0, 0, 0] + else: + rgbw_normalized = [round(x * 255 / brightness) for x in rgbw] + self._attr_brightness = brightness + self._attr_rgbw_color = tuple(rgbw_normalized) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index ea5eeb5a690..f51c79168d7 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pykulersky import pytest +from pytest import approx from homeassistant import setup from homeassistant.components.kulersky.const import ( @@ -17,14 +18,9 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, - COLOR_MODE_HS, COLOR_MODE_RGBW, SCAN_INTERVAL, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -72,12 +68,10 @@ async def test_init(hass, mock_light): """Test platform setup.""" state = hass.states.get("light.bedroom") assert state.state == STATE_OFF - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, } with patch.object(hass.loop, "stop"): @@ -129,7 +123,7 @@ async def test_light_turn_on(hass, mock_light): await hass.async_block_till_done() mock_light.set_color.assert_called_with(255, 255, 255, 255) - mock_light.get_color.return_value = (50, 50, 50, 255) + mock_light.get_color.return_value = (50, 50, 50, 50) await hass.services.async_call( "light", "turn_on", @@ -137,9 +131,33 @@ async def test_light_turn_on(hass, mock_light): blocking=True, ) await hass.async_block_till_done() - mock_light.set_color.assert_called_with(50, 50, 50, 255) + mock_light.set_color.assert_called_with(50, 50, 50, 50) - mock_light.get_color.return_value = (50, 45, 25, 255) + mock_light.get_color.return_value = (50, 25, 13, 6) + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_RGBW_COLOR: (255, 128, 64, 32)}, + blocking=True, + ) + await hass.async_block_till_done() + mock_light.set_color.assert_called_with(50, 25, 13, 6) + + # RGB color is converted to RGBW by assigning the white component to the white + # channel, see color_rgb_to_rgbw + mock_light.get_color.return_value = (0, 17, 50, 17) + await hass.services.async_call( + "light", + "turn_on", + {ATTR_ENTITY_ID: "light.bedroom", ATTR_RGB_COLOR: (64, 128, 255)}, + blocking=True, + ) + await hass.async_block_till_done() + mock_light.set_color.assert_called_with(0, 17, 50, 17) + + # HS color is converted to RGBW by assigning the white component to the white + # channel, see color_rgb_to_rgbw + mock_light.get_color.return_value = (50, 41, 0, 50) await hass.services.async_call( "light", "turn_on", @@ -147,18 +165,7 @@ async def test_light_turn_on(hass, mock_light): blocking=True, ) await hass.async_block_till_done() - - mock_light.set_color.assert_called_with(50, 45, 25, 255) - - mock_light.get_color.return_value = (220, 201, 110, 180) - await hass.services.async_call( - "light", - "turn_on", - {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180}, - blocking=True, - ) - await hass.async_block_till_done() - mock_light.set_color.assert_called_with(50, 45, 25, 180) + mock_light.set_color.assert_called_with(50, 41, 0, 50) async def test_light_turn_off(hass, mock_light): @@ -180,12 +187,10 @@ async def test_light_update(hass, mock_light): state = hass.states.get("light.bedroom") assert state.state == STATE_OFF - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, } # Test an exception during discovery @@ -196,12 +201,50 @@ async def test_light_update(hass, mock_light): state = hass.states.get("light.bedroom") assert state.state == STATE_UNAVAILABLE - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, + } + + mock_light.get_color.side_effect = None + mock_light.get_color.return_value = (80, 160, 255, 0) + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert dict(state.attributes) == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (approx(212.571), approx(68.627)), + ATTR_RGB_COLOR: (80, 160, 255), + ATTR_RGBW_COLOR: (80, 160, 255, 0), + ATTR_XY_COLOR: (approx(0.17), approx(0.193)), + } + + mock_light.get_color.side_effect = None + mock_light.get_color.return_value = (80, 160, 200, 255) + utcnow = utcnow + SCAN_INTERVAL + async_fire_time_changed(hass, utcnow) + await hass.async_block_till_done() + + state = hass.states.get("light.bedroom") + assert state.state == STATE_ON + assert dict(state.attributes) == { + ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (approx(199.701), approx(26.275)), + ATTR_RGB_COLOR: (188, 233, 255), + ATTR_RGBW_COLOR: (80, 160, 200, 255), + ATTR_XY_COLOR: (approx(0.259), approx(0.306)), } mock_light.get_color.side_effect = None @@ -212,17 +255,14 @@ async def test_light_update(hass, mock_light): state = hass.states.get("light.bedroom") assert state.state == STATE_ON - assert state.attributes == { + assert dict(state.attributes) == { ATTR_FRIENDLY_NAME: "Bedroom", - ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE, + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW], + ATTR_SUPPORTED_FEATURES: 0, ATTR_COLOR_MODE: COLOR_MODE_RGBW, - ATTR_BRIGHTNESS: 200, - ATTR_HS_COLOR: (200, 60), - ATTR_RGB_COLOR: (102, 203, 255), - ATTR_RGBW_COLOR: (102, 203, 255, 240), - ATTR_WHITE_VALUE: 240, - ATTR_XY_COLOR: (0.184, 0.261), + ATTR_BRIGHTNESS: 240, + ATTR_HS_COLOR: (approx(200.0), approx(27.059)), + ATTR_RGB_COLOR: (186, 232, 255), + ATTR_RGBW_COLOR: (85, 170, 212, 255), + ATTR_XY_COLOR: (approx(0.257), approx(0.305)), } From b11af5e6f8e8bd7fcf7a6687248f672cc7c992ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 16:39:25 +0200 Subject: [PATCH 656/750] Fix Garmin Connect sensor dependency import (#52306) --- homeassistant/components/garmin_connect/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index eb1690c9765..96f352c75b4 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from garminconnect_aio import ( +from garminconnect_ha import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, From 897f5d9247f113180dfe23f00b333775af9d53bb Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 29 Jun 2021 16:54:38 +0100 Subject: [PATCH 657/750] Coinbase code quality improvements from review (#52307) * Fix breaking loop if single bad currency * Remove unneeded update * Reduce executor calls and use helper * Avoid setting up integration when not needed in test * Remove defunct info from strings * Move already configured check * Move instance update out of data class init --- homeassistant/components/coinbase/__init__.py | 22 +++++++++++-------- .../components/coinbase/config_flow.py | 20 ++++++++++------- homeassistant/components/coinbase/sensor.py | 3 +-- .../components/coinbase/strings.json | 6 ++--- .../components/coinbase/translations/en.json | 6 ++--- tests/components/coinbase/test_config_flow.py | 9 +++++++- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 0351ddf19d1..eb4370a9534 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -66,17 +66,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coinbase from a config entry.""" - client = await hass.async_add_executor_job( - Client, - entry.data[CONF_API_KEY], - entry.data[CONF_API_TOKEN], + instance = await hass.async_add_executor_job( + create_and_update_instance, entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN] ) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( - CoinbaseData, client - ) + hass.data[DOMAIN][entry.entry_id] = instance hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -92,6 +88,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok +def create_and_update_instance(api_key, api_token): + """Create and update a Coinbase Data instance.""" + client = Client(api_key, api_token) + instance = CoinbaseData(client) + instance.update() + return instance + + def get_accounts(client): """Handle paginated accounts.""" response = client.get_accounts() @@ -113,8 +117,8 @@ class CoinbaseData: """Init the coinbase data object.""" self.client = client - self.accounts = get_accounts(self.client) - self.exchange_rates = self.client.get_exchange_rates() + self.accounts = None + self.exchange_rates = None self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index fe0bff799c6..adfa9977518 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -33,17 +33,20 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +def get_user_from_client(api_key, api_token): + """Get the user name from Coinbase API credentials.""" + client = Client(api_key, api_token) + user = client.get_current_user() + return user + + async def validate_api(hass: core.HomeAssistant, data): """Validate the credentials.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_API_KEY] == data[CONF_API_KEY]: - raise AlreadyConfigured try: - client = await hass.async_add_executor_job( - Client, data[CONF_API_KEY], data[CONF_API_TOKEN] + user = await hass.async_add_executor_job( + get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN] ) - user = await hass.async_add_executor_job(client.get_current_user) except AuthenticationError as error: raise InvalidAuth from error except ConnectionError as error: @@ -89,6 +92,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]}) + options = {} if CONF_OPTIONS in user_input: @@ -96,8 +101,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_api(self.hass, user_input) - except AlreadyConfigured: - return self.async_abort(reason="already_configured") except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -115,6 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, config): """Handle import of Coinbase config from YAML.""" + cleaned_data = { CONF_API_KEY: config[CONF_API_KEY], CONF_API_TOKEN: config[CONF_YAML_API_TOKEN], diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index b30fbabf8ef..5febfe8a978 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -36,7 +36,6 @@ ATTRIBUTION = "Data provided by coinbase.com" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Coinbase sensor platform.""" instance = hass.data[DOMAIN][config_entry.entry_id] - hass.async_add_executor_job(instance.update) entities = [] @@ -58,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "your settings in Coinbase's developer tools", currency, ) - break + continue entities.append(AccountSensor(instance, currency)) if CONF_EXCHANGE_RATES in config_entry.options: diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index 5988f2a49a9..399bfbd894a 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -3,12 +3,10 @@ "step": { "user": { "title": "Coinbase API Key Details", - "description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")", + "description": "Please enter the details of your API key as provided by Coinbase.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" } } }, diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index e17d9c07c5a..fa90f5d82b7 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -12,11 +12,9 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" }, - "description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")", + "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" } } diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index a03f423d852..dc036d23a6f 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -296,7 +296,12 @@ async def test_yaml_import(hass): ), patch( "coinbase.wallet.client.Client.get_exchange_rates", return_value=mock_get_exchange_rates(), - ): + ), patch( + "homeassistant.components.coinbase.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.coinbase.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) @@ -307,6 +312,8 @@ async def test_yaml_import(hass): CONF_CURRENCIES: ["BTC", "USD"], CONF_EXCHANGE_RATES: ["ATOM", "BTC"], } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_yaml_existing(hass): From 7959225fef303245fe1460c8f4f2fb46c904b6ad Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 29 Jun 2021 17:57:34 +0200 Subject: [PATCH 658/750] Add switch platform to Fritz (#51610) * Add switch platform to Fritz * Fix tests * Pylint * Small fix * Bump fritzprofiles to fix log level and identifier * Fix different WiFi networks with same name * Changed exposed attributes * Moved to extra_state * Remove redundant lambda * Add missing wait * Removed identifiers * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Add mapping dict * Device Profile disabled by default * Heavy cleanup * Tweak * Bug fix * Update homeassistant/components/fritz/switch.py Co-authored-by: Aaron David Schneider * Fix port forward switching + small log improvement * Cleanup from old approach * Handle port mapping hot removal (from device) * Minor fixes * Typying * Removed lambda call * Last missing strict typing * Split get entities * Func rename * Move FritzBoxBaseSwitch to switch.py * Removed lambda * Update homeassistant/components/fritz/common.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/common.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Fixes after applying comments * Remvoed redundant try block * Removed broad-except * Optimized async/sync switch * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Address remaining comments * Optimize return list * More optimization for return lists * Some missing strict typing * Redundant typing * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Wrong if * Introduce const for profile status * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/fritz/switch.py Co-authored-by: J. Nick Koston * Fix mypy * Switch back to get_local_ip() * Address latest comments Co-authored-by: J. Nick Koston Co-authored-by: Aaron David Schneider --- .coveragerc | 1 + homeassistant/components/fritz/common.py | 24 +- homeassistant/components/fritz/const.py | 10 +- homeassistant/components/fritz/manifest.json | 5 +- homeassistant/components/fritz/switch.py | 639 +++++++++++++++++++ requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/fritz/test_config_flow.py | 57 +- 8 files changed, 735 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/fritz/switch.py diff --git a/.coveragerc b/.coveragerc index 6fb95b595cc..eb0fbdc1fcf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -343,6 +343,7 @@ omit = homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py + homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index a255ef6439f..776c7a7a22e 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta import logging from types import MappingProxyType -from typing import Any, TypedDict +from typing import Any, Callable, TypedDict from fritzconnection import FritzConnection from fritzconnection.core.exceptions import ( @@ -15,6 +15,7 @@ from fritzconnection.core.exceptions import ( ) from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from fritzprofiles import FritzProfileSwitch, get_all_profiles from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, @@ -44,7 +45,7 @@ _LOGGER = logging.getLogger(__name__) class ClassSetupMissing(Exception): """Raised when a Class func is called before setup.""" - def __init__(self): + def __init__(self) -> None: """Init custom exception.""" super().__init__("Function called before Class setup") @@ -85,6 +86,7 @@ class FritzBoxTools: self._unique_id: str | None = None self.connection: FritzConnection = None self.fritz_hosts: FritzHosts = None + self.fritz_profiles: dict[str, FritzProfileSwitch] = {} self.fritz_status: FritzStatus = None self.hass = hass self.host = host @@ -117,6 +119,13 @@ class FritzBoxTools: self._model = info.get("NewModelName") self._sw_version = info.get("NewSoftwareVersion") + self.fritz_profiles = { + profile: FritzProfileSwitch( + "http://" + self.host, self.username, self.password, profile + ) + for profile in get_all_profiles(self.host, self.username, self.password) + } + async def async_start(self, options: MappingProxyType[str, Any]) -> None: """Start FritzHosts connection.""" self.fritz_hosts = FritzHosts(fc=self.connection) @@ -306,6 +315,17 @@ class FritzDevice: return self._last_activity +class SwitchInfo(TypedDict): + """FRITZ!Box switch info class.""" + + description: str + friendly_name: str + icon: str + type: str + callback_update: Callable + callback_switch: Callable + + class FritzBoxBaseEntity: """Fritz host entity base class.""" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 266e24b6be3..776c7a7dafa 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,7 +2,7 @@ DOMAIN = "fritz" -PLATFORMS = ["binary_sensor", "device_tracker", "sensor"] +PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] DATA_FRITZ = "fritz_data" @@ -19,6 +19,14 @@ FRITZ_SERVICES = "fritz_services" SERVICE_REBOOT = "reboot" SERVICE_RECONNECT = "reconnect" +SWITCH_PROFILE_STATUS_OFF = "never" +SWITCH_PROFILE_STATUS_ON = "unlimited" + +SWITCH_TYPE_DEFLECTION = "CallDeflection" +SWITCH_TYPE_DEVICEPROFILE = "DeviceProfile" +SWITCH_TYPE_PORTFORWARD = "PortForward" +SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" + TRACKER_SCAN_INTERVAL = 30 UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 1158ea2e797..d1c096a2ef5 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,8 +3,11 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.4.2" + "fritzconnection==1.4.2", + "fritzprofiles==0.6.1", + "xmltodict==0.12.0" ], + "dependencies": ["network"], "codeowners": [ "@mammuth", "@AaronDavidSchneider", diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py new file mode 100644 index 00000000000..09bae36a29c --- /dev/null +++ b/homeassistant/components/fritz/switch.py @@ -0,0 +1,639 @@ +"""Switches for AVM Fritz!Box functions.""" +from __future__ import annotations + +from collections import OrderedDict +from functools import partial +import logging +from typing import Any + +from fritzconnection.core.exceptions import ( + FritzActionError, + FritzActionFailedError, + FritzConnectionException, + FritzSecurityError, + FritzServiceError, +) +import xmltodict + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import get_local_ip, slugify + +from .common import FritzBoxBaseEntity, FritzBoxTools, SwitchInfo +from .const import ( + DOMAIN, + SWITCH_PROFILE_STATUS_OFF, + SWITCH_PROFILE_STATUS_ON, + SWITCH_TYPE_DEFLECTION, + SWITCH_TYPE_DEVICEPROFILE, + SWITCH_TYPE_PORTFORWARD, + SWITCH_TYPE_WIFINETWORK, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_service_call_action( + fritzbox_tools: FritzBoxTools, + service_name: str, + service_suffix: str | None, + action_name: str, + **kwargs: Any, +) -> None | dict: + """Return service details.""" + return await fritzbox_tools.hass.async_add_executor_job( + partial( + service_call_action, + fritzbox_tools, + service_name, + service_suffix, + action_name, + **kwargs, + ) + ) + + +def service_call_action( + fritzbox_tools: FritzBoxTools, + service_name: str, + service_suffix: str | None, + action_name: str, + **kwargs: Any, +) -> dict | None: + """Return service details.""" + + if f"{service_name}{service_suffix}" not in fritzbox_tools.connection.services: + return None + + try: + return fritzbox_tools.connection.call_action( + f"{service_name}:{service_suffix}", + action_name, + **kwargs, + ) + except FritzSecurityError: + _LOGGER.error( + "Authorization Error: Please check the provided credentials and verify that you can log into the web interface", + exc_info=True, + ) + return None + except (FritzActionError, FritzActionFailedError, FritzServiceError): + _LOGGER.error( + "Service/Action Error: cannot execute service %s", + service_name, + exc_info=True, + ) + return None + except FritzConnectionException: + _LOGGER.error( + "Connection Error: Please check the device is properly configured for remote login", + exc_info=True, + ) + return None + + +def get_deflections( + fritzbox_tools: FritzBoxTools, service_name: str +) -> list[OrderedDict[Any, Any]] | None: + """Get deflection switch info.""" + + deflection_list = service_call_action( + fritzbox_tools, + service_name, + "1", + "GetDeflections", + ) + + if not deflection_list: + return [] + + return [xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"]] + + +def deflection_entities_list( + fritzbox_tools: FritzBoxTools, device_friendly_name: str +) -> list[FritzBoxDeflectionSwitch]: + """Get list of deflection entities.""" + + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION) + + service_name = "X_AVM-DE_OnTel" + deflections_response = service_call_action( + fritzbox_tools, service_name, "1", "GetNumberOfDeflections" + ) + if not deflections_response: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) + return [] + + _LOGGER.debug( + "Specific %s response: GetNumberOfDeflections=%s", + SWITCH_TYPE_DEFLECTION, + deflections_response, + ) + + if deflections_response["NewNumberOfDeflections"] == 0: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) + return [] + + deflection_list = get_deflections(fritzbox_tools, service_name) + if deflection_list is None: + return [] + + return [ + FritzBoxDeflectionSwitch( + fritzbox_tools, device_friendly_name, dict_of_deflection + ) + for dict_of_deflection in deflection_list + ] + + +def port_entities_list( + fritzbox_tools: FritzBoxTools, device_friendly_name: str +) -> list[FritzBoxPortSwitch]: + """Get list of port forwarding entities.""" + + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD) + entities_list: list = [] + service_name = "Layer3Forwarding" + connection_type = service_call_action( + fritzbox_tools, service_name, "1", "GetDefaultConnectionService" + ) + if not connection_type: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD) + return [] + + # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1" + con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2] + + # Query port forwardings and setup a switch for each forward for the current device + resp = service_call_action( + fritzbox_tools, con_type, "1", "GetPortMappingNumberOfEntries" + ) + if not resp: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) + return [] + + port_forwards_count: int = resp["NewPortMappingNumberOfEntries"] + + _LOGGER.debug( + "Specific %s response: GetPortMappingNumberOfEntries=%s", + SWITCH_TYPE_PORTFORWARD, + port_forwards_count, + ) + + local_ip = get_local_ip() + _LOGGER.debug("IP source for %s is %s", fritzbox_tools.host, local_ip) + + for i in range(port_forwards_count): + + portmap = service_call_action( + fritzbox_tools, + con_type, + "1", + "GetGenericPortMappingEntry", + NewPortMappingIndex=i, + ) + + if not portmap: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) + continue + + _LOGGER.debug( + "Specific %s response: GetGenericPortMappingEntry=%s", + SWITCH_TYPE_PORTFORWARD, + portmap, + ) + + # We can only handle port forwards of the given device + if portmap["NewInternalClient"] == local_ip: + entities_list.append( + FritzBoxPortSwitch( + fritzbox_tools, + device_friendly_name, + portmap, + i, + con_type, + ) + ) + + return entities_list + + +def profile_entities_list( + fritzbox_tools: FritzBoxTools, device_friendly_name: str +) -> list[FritzBoxProfileSwitch]: + """Get list of profile entities.""" + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEVICEPROFILE) + if len(fritzbox_tools.fritz_profiles) <= 0: + _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEVICEPROFILE) + return [] + + return [ + FritzBoxProfileSwitch(fritzbox_tools, device_friendly_name, profile) + for profile in fritzbox_tools.fritz_profiles.keys() + ] + + +def wifi_entities_list( + fritzbox_tools: FritzBoxTools, device_friendly_name: str +) -> list[FritzBoxWifiSwitch]: + """Get list of wifi entities.""" + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK) + std_table = {"ac": "5Ghz", "n": "2.4Ghz"} + networks: dict = {} + for i in range(4): + if not ("WLANConfiguration" + str(i)) in fritzbox_tools.connection.services: + continue + + network_info = service_call_action( + fritzbox_tools, "WLANConfiguration", str(i), "GetInfo" + ) + if network_info: + ssid = network_info["NewSSID"] + if ssid in networks.values(): + networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' + else: + networks[i] = ssid + + return [ + FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, networks[net]) + for net in networks + ] + + +def all_entities_list( + fritzbox_tools: FritzBoxTools, device_friendly_name: str +) -> list[Entity]: + """Get a list of all entities.""" + return [ + *deflection_entities_list(fritzbox_tools, device_friendly_name), + *port_entities_list(fritzbox_tools, device_friendly_name), + *profile_entities_list(fritzbox_tools, device_friendly_name), + *wifi_entities_list(fritzbox_tools, device_friendly_name), + ] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up entry.""" + _LOGGER.debug("Setting up switches") + fritzbox_tools: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + + _LOGGER.debug("Fritzbox services: %s", fritzbox_tools.connection.services) + + entities_list = await hass.async_add_executor_job( + all_entities_list, fritzbox_tools, entry.title + ) + async_add_entities(entities_list) + + +class FritzBoxBaseSwitch(FritzBoxBaseEntity): + """Fritz switch base class.""" + + def __init__( + self, + fritzbox_tools: FritzBoxTools, + device_friendly_name: str, + switch_info: SwitchInfo, + ) -> None: + """Init Fritzbox port switch.""" + super().__init__(fritzbox_tools, device_friendly_name) + + self._description = switch_info["description"] + self._friendly_name = switch_info["friendly_name"] + self._icon = switch_info["icon"] + self._type = switch_info["type"] + self._update = switch_info["callback_update"] + self._switch = switch_info["callback_switch"] + + self._name = f"{self._friendly_name} {self._description}" + self._unique_id = ( + f"{self._fritzbox_tools.unique_id}-{slugify(self._description)}" + ) + + self._attributes: dict[str, str] = {} + self._is_available = True + + self._attr_is_on = False + + @property + def name(self) -> str: + """Return name.""" + return self._name + + @property + def icon(self) -> str: + """Return name.""" + return self._icon + + @property + def unique_id(self) -> str: + """Return unique id.""" + return self._unique_id + + @property + def available(self) -> bool: + """Return availability.""" + return self._is_available + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return device attributes.""" + return self._attributes + + async def async_update(self) -> None: + """Update data.""" + _LOGGER.debug("Updating '%s' (%s) switch state", self.name, self._type) + await self._update() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self._async_handle_turn_on_off(turn_on=True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self._async_handle_turn_on_off(turn_on=False) + + async def _async_handle_turn_on_off(self, turn_on: bool) -> bool: + """Handle switch state change request.""" + await self._switch(turn_on) + self._attr_is_on = turn_on + return True + + +class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): + """Defines a FRITZ!Box Tools PortForward switch.""" + + def __init__( + self, + fritzbox_tools: FritzBoxTools, + device_friendly_name: str, + port_mapping: dict[str, Any] | None, + idx: int, + connection_type: str, + ) -> None: + """Init Fritzbox port switch.""" + self._fritzbox_tools = fritzbox_tools + + self._attributes = {} + self.connection_type = connection_type + self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0} + self._idx = idx # needed for update routine + + if port_mapping is None: + return + + switch_info = SwitchInfo( + description=f'Port forward {port_mapping["NewPortMappingDescription"]}', + friendly_name=device_friendly_name, + icon="mdi:check-network", + type=SWITCH_TYPE_PORTFORWARD, + callback_update=self._async_fetch_update, + callback_switch=self._async_handle_port_switch_on_off, + ) + super().__init__(fritzbox_tools, device_friendly_name, switch_info) + + async def _async_fetch_update(self) -> None: + """Fetch updates.""" + + self.port_mapping = await async_service_call_action( + self._fritzbox_tools, + self.connection_type, + "1", + "GetGenericPortMappingEntry", + NewPortMappingIndex=self._idx, + ) + _LOGGER.debug( + "Specific %s response: %s", SWITCH_TYPE_PORTFORWARD, self.port_mapping + ) + if self.port_mapping is None: + self._is_available = False + return + + self._attr_is_on = self.port_mapping["NewEnabled"] is True + self._is_available = True + + attributes_dict = { + "NewInternalClient": "internalIP", + "NewInternalPort": "internalPort", + "NewExternalPort": "externalPort", + "NewProtocol": "protocol", + "NewPortMappingDescription": "description", + } + + for key in attributes_dict: + self._attributes[attributes_dict[key]] = self.port_mapping[key] + + async def _async_handle_port_switch_on_off(self, turn_on: bool) -> bool: + + if self.port_mapping is None: + return False + + self.port_mapping["NewEnabled"] = "1" if turn_on else "0" + + resp = await async_service_call_action( + self._fritzbox_tools, + self.connection_type, + "1", + "AddPortMapping", + **self.port_mapping, + ) + + return bool(resp is not None) + + +class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): + """Defines a FRITZ!Box Tools PortForward switch.""" + + def __init__( + self, + fritzbox_tools: FritzBoxTools, + device_friendly_name: str, + dict_of_deflection: Any, + ) -> None: + """Init Fritxbox Deflection class.""" + self._fritzbox_tools: FritzBoxTools = fritzbox_tools + + self.dict_of_deflection = dict_of_deflection + self._attributes = {} + self.id = int(self.dict_of_deflection["DeflectionId"]) + + switch_info = SwitchInfo( + description=f"Call deflection {self.id}", + friendly_name=device_friendly_name, + icon="mdi:phone-forward", + type=SWITCH_TYPE_DEFLECTION, + callback_update=self._async_fetch_update, + callback_switch=self._async_switch_on_off_executor, + ) + super().__init__(self._fritzbox_tools, device_friendly_name, switch_info) + + async def _async_fetch_update(self) -> None: + """Fetch updates.""" + + resp = await async_service_call_action( + self._fritzbox_tools, "X_AVM-DE_OnTel", "1", "GetDeflections" + ) + if not resp: + self._is_available = False + return + + self.dict_of_deflection = xmltodict.parse(resp["NewDeflectionList"])["List"][ + "Item" + ] + if isinstance(self.dict_of_deflection, list): + self.dict_of_deflection = self.dict_of_deflection[self.id] + + _LOGGER.debug( + "Specific %s response: NewDeflectionList=%s", + SWITCH_TYPE_DEFLECTION, + self.dict_of_deflection, + ) + + self._attr_is_on = self.dict_of_deflection["Enable"] == "1" + self._is_available = True + + self._attributes["Type"] = self.dict_of_deflection["Type"] + self._attributes["Number"] = self.dict_of_deflection["Number"] + self._attributes["DeflectionToNumber"] = self.dict_of_deflection[ + "DeflectionToNumber" + ] + # Return mode sample: "eImmediately" + self._attributes["Mode"] = self.dict_of_deflection["Mode"][1:] + self._attributes["Outgoing"] = self.dict_of_deflection["Outgoing"] + self._attributes["PhonebookID"] = self.dict_of_deflection["PhonebookID"] + + async def _async_switch_on_off_executor(self, turn_on: bool) -> None: + """Handle deflection switch.""" + await async_service_call_action( + self._fritzbox_tools, + "X_AVM-DE_OnTel", + "1", + "SetDeflectionEnable", + NewDeflectionId=self.id, + NewEnable="1" if turn_on else "0", + ) + + +class FritzBoxProfileSwitch(FritzBoxBaseSwitch, SwitchEntity): + """Defines a FRITZ!Box Tools DeviceProfile switch.""" + + def __init__( + self, fritzbox_tools: FritzBoxTools, device_friendly_name: str, profile: str + ) -> None: + """Init Fritz profile.""" + self._fritzbox_tools: FritzBoxTools = fritzbox_tools + self.profile = profile + + switch_info = SwitchInfo( + description=f"Profile {profile}", + friendly_name=device_friendly_name, + icon="mdi:router-wireless-settings", + type=SWITCH_TYPE_DEVICEPROFILE, + callback_update=self._async_fetch_update, + callback_switch=self._async_switch_on_off_executor, + ) + super().__init__(self._fritzbox_tools, device_friendly_name, switch_info) + + async def _async_fetch_update(self) -> None: + """Update data.""" + try: + status = await self.hass.async_add_executor_job( + self._fritzbox_tools.fritz_profiles[self.profile].get_state + ) + _LOGGER.debug( + "Specific %s response: get_State()=%s", + SWITCH_TYPE_DEVICEPROFILE, + status, + ) + if status == SWITCH_PROFILE_STATUS_OFF: + self._attr_is_on = False + self._is_available = True + elif status == SWITCH_PROFILE_STATUS_ON: + self._attr_is_on = True + self._is_available = True + else: + self._is_available = False + except Exception: # pylint: disable=broad-except + _LOGGER.error("Could not get %s state", self.name, exc_info=True) + self._is_available = False + + async def _async_switch_on_off_executor(self, turn_on: bool) -> None: + """Handle profile switch.""" + state = SWITCH_PROFILE_STATUS_ON if turn_on else SWITCH_PROFILE_STATUS_OFF + await self.hass.async_add_executor_job( + self._fritzbox_tools.fritz_profiles[self.profile].set_state, state + ) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + +class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): + """Defines a FRITZ!Box Tools Wifi switch.""" + + def __init__( + self, + fritzbox_tools: FritzBoxTools, + device_friendly_name: str, + network_num: int, + network_name: str, + ) -> None: + """Init Fritz Wifi switch.""" + self._fritzbox_tools = fritzbox_tools + + self._attributes = {} + self._network_num = network_num + + switch_info = SwitchInfo( + description=f"Wi-Fi {network_name}", + friendly_name=device_friendly_name, + icon="mdi:wifi", + type=SWITCH_TYPE_WIFINETWORK, + callback_update=self._async_fetch_update, + callback_switch=self._async_switch_on_off_executor, + ) + super().__init__(self._fritzbox_tools, device_friendly_name, switch_info) + + async def _async_fetch_update(self) -> None: + """Fetch updates.""" + + wifi_info = await async_service_call_action( + self._fritzbox_tools, + "WLANConfiguration", + str(self._network_num), + "GetInfo", + ) + _LOGGER.debug( + "Specific %s response: GetInfo=%s", SWITCH_TYPE_WIFINETWORK, wifi_info + ) + + if wifi_info is None: + self._is_available = False + return + + self._attr_is_on = wifi_info["NewEnable"] is True + self._is_available = True + + std = wifi_info["NewStandard"] + self._attributes["standard"] = std if std else None + self._attributes["BSSID"] = wifi_info["NewBSSID"] + self._attributes["mac_address_control"] = wifi_info[ + "NewMACAddressControlEnabled" + ] + + async def _async_switch_on_off_executor(self, turn_on: bool) -> None: + """Handle wifi switch.""" + await async_service_call_action( + self._fritzbox_tools, + "WLANConfiguration", + str(self._network_num), + "SetEnable", + NewEnable="1" if turn_on else "0", + ) diff --git a/requirements_all.txt b/requirements_all.txt index b95c9a26ae6..27d81dd8eec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,6 +640,9 @@ freesms==0.2.0 # homeassistant.components.fritzbox_netmonitor fritzconnection==1.4.2 +# homeassistant.components.fritz +fritzprofiles==0.6.1 + # homeassistant.components.google_translate gTTS==2.2.3 @@ -2390,6 +2393,7 @@ xboxapi==2.0.1 xknx==0.18.7 # homeassistant.components.bluesound +# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 933122e1c49..ff553882fe2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ freebox-api==0.0.10 # homeassistant.components.fritzbox_netmonitor fritzconnection==1.4.2 +# homeassistant.components.fritz +fritzprofiles==0.6.1 + # homeassistant.components.google_translate gTTS==2.2.3 @@ -1308,6 +1311,7 @@ xbox-webapi==2.0.11 xknx==0.18.7 # homeassistant.components.bluesound +# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index a68aee2edff..1551a508277 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -56,6 +56,8 @@ MOCK_SSDP_DATA = { ATTR_UPNP_UDN: "uuid:only-a-test", } +MOCK_REQUEST = b'xxxxxxxxxxxxxxxxxxxxxxxx0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2FakeFritzUser\n' + @pytest.fixture() def fc_class_mock(): @@ -72,7 +74,16 @@ async def test_user(hass: HomeAssistant, fc_class_mock): side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get, patch( + "requests.post" + ) as mock_request_post: + + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -106,7 +117,16 @@ async def test_user_already_configured(hass: HomeAssistant, fc_class_mock): with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "requests.get" + ) as mock_request_get, patch( + "requests.post" + ) as mock_request_post: + + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -202,7 +222,16 @@ async def test_reauth_successful(hass: HomeAssistant, fc_class_mock): side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get, patch( + "requests.post" + ) as mock_request_post: + + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( DOMAIN, @@ -355,7 +384,16 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock): side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get, patch( + "requests.post" + ) as mock_request_post: + + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -411,7 +449,16 @@ async def test_import(hass: HomeAssistant, fc_class_mock): side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get, patch( + "requests.post" + ) as mock_request_post: + + mock_request_get.return_value.status_code = 200 + mock_request_get.return_value.content = MOCK_REQUEST + mock_request_post.return_value.status_code = 200 + mock_request_post.return_value.text = MOCK_REQUEST result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_CONFIG From 6afa4d6914681fde7ece8f20c803f22efbd4b3cd Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 29 Jun 2021 19:14:34 +0200 Subject: [PATCH 659/750] Skip updating tplink bulb state if the new state not reported by the device (#52310) --- homeassistant/components/tplink/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index e5217cbc143..6d497812261 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -432,7 +432,10 @@ class TPLinkSmartBulb(LightEntity): self._is_setting_light_state = False if LIGHT_STATE_ERROR_MSG in light_state_params: raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG]) - self._light_state = self._light_state_from_params(light_state_params) + # Some devices do not report the new state in their responses, so we skip + # set here and wait for the next poll to update the values. See #47600 + if LIGHT_STATE_ON_OFF in light_state_params: + self._light_state = self._light_state_from_params(light_state_params) return except (SmartDeviceException, OSError): pass From 2576dd9da92b4d8d17dde716d01cf64e5e9d0b74 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Tue, 29 Jun 2021 19:15:50 +0200 Subject: [PATCH 660/750] Fix Todoist incorrect end date when task has no time (#52258) --- homeassistant/components/todoist/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 8b9379d2186..86aeff7c554 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -230,7 +230,7 @@ def _parse_due_date(data: dict, gmt_string) -> datetime: """Parse the due date dict into a datetime object.""" # Add time information to date only strings. if len(data["date"]) == 10: - data["date"] += "T00:00:00" + return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC) if dt.parse_datetime(data["date"]).tzinfo is None: data["date"] += gmt_string return dt.as_utc(dt.parse_datetime(data["date"])) From ba7ad8a58fd71e648917084a769543f5b5919b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 29 Jun 2021 19:16:43 +0200 Subject: [PATCH 661/750] Add Melcloud device class and state class (#52276) --- homeassistant/components/melcloud/sensor.py | 46 +++++++------------ .../melcloud/test_atw_zone_sensor.py | 16 +++++-- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 2c59763dc72..c1b7e5e8cbd 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -2,14 +2,19 @@ from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, - DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ) +from homeassistant.util import dt as dt_util from . import MelCloudDevice from .const import DOMAIN @@ -32,7 +37,7 @@ ATA_SENSORS = { ATTR_MEASUREMENT_NAME: "Energy", ATTR_ICON: "mdi:factory", ATTR_UNIT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter, }, @@ -116,40 +121,23 @@ class MelDeviceSensor(SensorEntity): def __init__(self, api: MelCloudDevice, measurement, definition): """Initialize the sensor.""" self._api = api - self._name_slug = api.name - self._measurement = measurement self._def = definition - @property - def unique_id(self): - """Return a unique ID.""" - return f"{self._api.device.serial}-{self._api.device.mac}-{self._measurement}" + self._attr_device_class = definition[ATTR_DEVICE_CLASS] + self._attr_icon = definition[ATTR_ICON] + self._attr_name = f"{api.name} {definition[ATTR_MEASUREMENT_NAME]}" + self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{measurement}" + self._attr_unit_of_measurement = definition[ATTR_UNIT] + self._attr_state_class = STATE_CLASS_MEASUREMENT - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._def[ATTR_ICON] - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name_slug} {self._def[ATTR_MEASUREMENT_NAME]}" + if self.device_class == DEVICE_CLASS_ENERGY: + self._attr_last_reset = dt_util.utc_from_timestamp(0) @property def state(self): """Return the state of the sensor.""" return self._def[ATTR_VALUE_FN](self._api) - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._def[ATTR_UNIT] - - @property - def device_class(self): - """Return device class.""" - return self._def[ATTR_DEVICE_CLASS] - async def async_update(self): """Retrieve latest state.""" await self._api.async_update() @@ -171,7 +159,7 @@ class AtwZoneSensor(MelDeviceSensor): full_measurement = f"{measurement}-zone-{zone.zone_index}" super().__init__(api, full_measurement, definition) self._zone = zone - self._name_slug = f"{api.name} {zone.name}" + self._attr_name = f"{api.name} {zone.name} {definition[ATTR_MEASUREMENT_NAME]}" @property def state(self): diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py index ac34a5ccc49..6e6487a3774 100644 --- a/tests/components/melcloud/test_atw_zone_sensor.py +++ b/tests/components/melcloud/test_atw_zone_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.melcloud.sensor import AtwZoneSensor +from homeassistant.components.melcloud.sensor import ATW_ZONE_SENSORS, AtwZoneSensor @pytest.fixture @@ -34,8 +34,18 @@ def mock_zone_2(): def test_zone_unique_ids(mock_device, mock_zone_1, mock_zone_2): """Test unique id generation correctness.""" - sensor_1 = AtwZoneSensor(mock_device, mock_zone_1, "room_temperature", {}) + sensor_1 = AtwZoneSensor( + mock_device, + mock_zone_1, + "room_temperature", + ATW_ZONE_SENSORS["room_temperature"], + ) assert sensor_1.unique_id == "1234-11:11:11:11:11:11-room_temperature" - sensor_2 = AtwZoneSensor(mock_device, mock_zone_2, "room_temperature", {}) + sensor_2 = AtwZoneSensor( + mock_device, + mock_zone_2, + "room_temperature", + ATW_ZONE_SENSORS["flow_temperature"], + ) assert sensor_2.unique_id == "1234-11:11:11:11:11:11-room_temperature-zone-2" From b43d0877e70b5154696951f0d2ad1d66826cb6d7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 29 Jun 2021 19:53:57 +0200 Subject: [PATCH 662/750] ESPHome Migrate to dataclasses (#52305) --- homeassistant/components/esphome/__init__.py | 2 +- .../components/esphome/entry_data.py | 56 ++++++++----------- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 27 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2490c0bdbd3..b91197b019c 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -555,7 +555,7 @@ async def _register_service( "example": "['Example text', 'Another example']", "selector": {"object": {}}, }, - }[arg.type_] + }[arg.type] schema[vol.Required(arg.name)] = metadata["validator"] fields[arg.name] = { "name": arg.name, diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 49655de8fb7..1e19780ed0b 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable from aioesphomeapi import ( @@ -22,7 +23,6 @@ from aioesphomeapi import ( TextSensorInfo, UserService, ) -import attr from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -49,32 +49,31 @@ INFO_TYPE_TO_PLATFORM = { } -@attr.s +@dataclass class RuntimeEntryData: """Store runtime data for esphome config entries.""" - _storage_contents: dict | None = None - - entry_id: str = attr.ib() - client: APIClient = attr.ib() - store: Store = attr.ib() - state: dict[str, dict[str, Any]] = attr.ib(factory=dict) - info: dict[str, dict[str, Any]] = attr.ib(factory=dict) + entry_id: str + client: APIClient + store: Store + state: dict[str, dict[str, Any]] = field(default_factory=dict) + info: dict[str, dict[str, Any]] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires # some static info to be accessible during removal (unique_id, maybe others) # If an entity can't find anything in the info array, it will look for info here. - old_info: dict[str, dict[str, Any]] = attr.ib(factory=dict) + old_info: dict[str, dict[str, Any]] = field(default_factory=dict) - services: dict[int, UserService] = attr.ib(factory=dict) - available: bool = attr.ib(default=False) - device_info: DeviceInfo | None = attr.ib(default=None) - api_version: APIVersion = attr.ib(factory=APIVersion) - cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list) - disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list) - loaded_platforms: set[str] = attr.ib(factory=set) - platform_load_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock) + services: dict[int, UserService] = field(default_factory=dict) + available: bool = False + device_info: DeviceInfo | None = None + api_version: APIVersion = field(default_factory=APIVersion) + cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) + disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + loaded_platforms: set[str] = field(default_factory=set) + platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) + _storage_contents: dict | None = None @callback def async_update_entity( @@ -142,20 +141,15 @@ class RuntimeEntryData: return [], [] self._storage_contents = restored.copy() - self.device_info = _attr_obj_from_dict( - DeviceInfo, **restored.pop("device_info") - ) - self.api_version = _attr_obj_from_dict( - APIVersion, **restored.pop("api_version", {}) - ) - + self.device_info = DeviceInfo.from_dict(restored.pop("device_info")) + self.api_version = APIVersion.from_dict(restored.pop("api_version")) infos = [] for comp_type, restored_infos in restored.items(): if comp_type not in COMPONENT_TYPE_TO_INFO: continue for info in restored_infos: cls = COMPONENT_TYPE_TO_INFO[comp_type] - infos.append(_attr_obj_from_dict(cls, **info)) + infos.append(cls.from_dict(info)) services = [] for service in restored.get("services", []): services.append(UserService.from_dict(service)) @@ -164,13 +158,13 @@ class RuntimeEntryData: async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" store_data = { - "device_info": attr.asdict(self.device_info), + "device_info": self.device_info.to_dict(), "services": [], - "api_version": attr.asdict(self.api_version), + "api_version": self.api_version.to_dict(), } for comp_type, infos in self.info.items(): - store_data[comp_type] = [attr.asdict(info) for info in infos.values()] + store_data[comp_type] = [info.to_dict() for info in infos.values()] for service in self.services.values(): store_data["services"].append(service.to_dict()) @@ -182,7 +176,3 @@ class RuntimeEntryData: return store_data self.store.async_delay_save(_memorized_storage, SAVE_DELAY) - - -def _attr_obj_from_dict(cls, **kwargs): - return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) if key in kwargs}) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 284fdc25956..e69299a4a43 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==3.1.0"], + "requirements": ["aioesphomeapi==4.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 27d81dd8eec..6d5d1aee10b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==3.1.0 +aioesphomeapi==4.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff553882fe2..c60ff0d2eb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==3.1.0 +aioesphomeapi==4.0.1 # homeassistant.components.flo aioflo==0.4.1 From 935f4d16a9ba0896240b452d629cdd9acf010501 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Jun 2021 12:57:28 -0500 Subject: [PATCH 663/750] Fix small inconsistencies in RainMachine vegetation and sprinkler types (#52313) --- homeassistant/components/rainmachine/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index d2dab8bd769..b0544b1adbe 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -84,7 +84,7 @@ SLOPE_TYPE_MAP = { SPRINKLER_TYPE_MAP = { 0: "Not Set", 1: "Popup Spray", - 2: "Rotors", + 2: "Rotors Low Rate", 3: "Surface Drip", 4: "Bubblers Drip", 5: "Rotors High Rate", @@ -101,9 +101,10 @@ VEGETATION_MAP = { 4: "Flowers", 5: "Vegetables", 6: "Citrus", - 7: "Trees and Bushes", + 7: "Bushes", 9: "Drought Tolerant Plants", 10: "Warm Season Grass", + 11: "Trees", 99: "Other", } From aac0180abf9c1c0e21d519b726654e378801149b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 19:59:25 +0200 Subject: [PATCH 664/750] Disable import of disabled eebrightbox in tests (#52314) --- tests/components/ee_brightbox/test_device_tracker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index e88a481648f..afe3897eff9 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -2,7 +2,8 @@ from datetime import datetime from unittest.mock import patch -from eebrightbox import EEBrightBoxException +# Integration is disabled +# from eebrightbox import EEBrightBoxException import pytest from homeassistant.components.device_tracker import DOMAIN @@ -46,7 +47,8 @@ def _configure_mock_get_devices(eebrightbox_mock): def _configure_mock_failed_config_check(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value - eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( + # Integration is disabled + eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( # noqa: F821 "Failed to connect to the router" ) From 853ca331e40ebdf852fa6f291d25915d631230ff Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Jun 2021 20:33:38 +0200 Subject: [PATCH 665/750] Stop build wheels for python38 (#52309) --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 1e12c8a1dff..8e97c61194b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,7 +65,6 @@ jobs: matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} tag: - - "3.8-alpine3.12" - "3.9-alpine3.13" steps: - name: Checkout the repository @@ -106,7 +105,6 @@ jobs: matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} tag: - - "3.8-alpine3.12" - "3.9-alpine3.13" steps: - name: Checkout the repository From ddef5d23149bf96af7cf0341f96f0303cde7c3c6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 Jun 2021 14:15:56 -0500 Subject: [PATCH 666/750] Refactor Tile entity unique ID migration to use helper (#52315) * Refactor Tile entity unique ID migration to use helper * Clarify Co-authored-by: Martin Hjelmare --- homeassistant/components/tile/__init__.py | 41 ++++++++++++----------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 2f00dabbcb6..7faefe4d275 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -6,8 +6,10 @@ from pytile import async_login from pytile.errors import InvalidAuthError, SessionExpiredError, TileError from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, entity_registry +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity_registry import async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency @@ -33,30 +35,31 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} - # The existence of shared Tiles across multiple accounts requires an entity ID - # change: - # - # Old: tile_{uuid} - # New: {username}_{uuid} - # - # Find any entities with the old format and update them: - ent_reg = entity_registry.async_get(hass) - for entity in [ - e - for e in ent_reg.entities.values() - if e.config_entry_id == entry.entry_id - and not e.unique_id.startswith(entry.data[CONF_USERNAME]) - ]: + @callback + def async_migrate_callback(entity_entry): + """ + Define a callback to migrate appropriate Tile entities to new unique IDs. + + Old: tile_{uuid} + New: {username}_{uuid} + """ + if entity_entry.unique_id.startswith(entry.data[CONF_USERNAME]): + return + new_unique_id = f"{entry.data[CONF_USERNAME]}_".join( - entity.unique_id.split(f"{DOMAIN}_") + entity_entry.unique_id.split(f"{DOMAIN}_") ) + LOGGER.debug( "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", - entity.entity_id, - entity.unique_id, + entity_entry.entity_id, + entity_entry.unique_id, new_unique_id, ) - ent_reg.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) + + return {"new_unique_id": new_unique_id} + + await async_migrate_entries(hass, entry.entry_id, async_migrate_callback) websession = aiohttp_client.async_get_clientsession(hass) From 7c39092aa852f4cf29e180b8d4dfa8dcd3bfc3c7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Jun 2021 21:50:25 +0200 Subject: [PATCH 667/750] Upgrade nmap tracker with forked package for compatibility (#52300) * Upgrade nmap tracker with forked package for compatibility * Bump to 0.7.0.2 * Bump cache version --- .github/workflows/ci.yaml | 2 +- homeassistant/components/nmap_tracker/manifest.json | 4 ++-- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 139fa614597..0809cf604cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ on: pull_request: ~ env: - CACHE_VERSION: 1 + CACHE_VERSION: 2 DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit SQLALCHEMY_WARN_20: 1 diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 3d0f9d9b014..ee05843c4fe 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -3,7 +3,7 @@ "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": [ - "python-nmap==0.6.4", + "netmap==0.7.0.2", "getmac==0.8.2", "ifaddr==0.1.7", "mac-vendor-lookup==0.1.11" @@ -11,4 +11,4 @@ "codeowners": ["@bdraco"], "iot_class": "local_polling", "config_flow": true -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 6d5d1aee10b..9dffcacf402 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,6 +1017,9 @@ netdata==0.2.0 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1863,9 +1866,6 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 -# homeassistant.components.nmap_tracker -python-nmap==0.6.4 - # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c60ff0d2eb0..cad87b46a2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -574,6 +574,9 @@ nessclient==0.9.15 # homeassistant.components.discovery netdisco==2.9.0 +# homeassistant.components.nmap_tracker +netmap==0.7.0.2 + # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1036,9 +1039,6 @@ python-miio==0.5.6 # homeassistant.components.nest python-nest==4.1.0 -# homeassistant.components.nmap_tracker -python-nmap==0.6.4 - # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 From 56d1bf255bd906fe3578f9421cc278a61ec6f4da Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Tue, 29 Jun 2021 20:34:09 +0000 Subject: [PATCH 668/750] Bump enturclient to v0.2.2 (#52321) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index ad522be9321..e2a5175211c 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.1"], + "requirements": ["enturclient==0.2.2"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9dffcacf402..60af0b35851 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,7 +560,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.1 +enturclient==0.2.2 # homeassistant.components.environment_canada env_canada==0.2.5 From 2eebfe6ff3788ca1bf43bfdcbccd5ab450e60ee7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jun 2021 10:50:29 -1000 Subject: [PATCH 669/750] Fix esphome startup with missing api_version key (#52324) --- homeassistant/components/esphome/entry_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 1e19780ed0b..f60d7cfefb5 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -142,7 +142,7 @@ class RuntimeEntryData: self._storage_contents = restored.copy() self.device_info = DeviceInfo.from_dict(restored.pop("device_info")) - self.api_version = APIVersion.from_dict(restored.pop("api_version")) + self.api_version = APIVersion.from_dict(restored.pop("api_version", {})) infos = [] for comp_type, restored_infos in restored.items(): if comp_type not in COMPONENT_TYPE_TO_INFO: From cca5964ac0839a3c07a336cda099c2275b2facde Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Jun 2021 23:30:13 +0200 Subject: [PATCH 670/750] Normalize pressure statistics to Pa (#52298) --- homeassistant/components/sensor/recorder.py | 15 +++++++++++++++ homeassistant/util/pressure.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index fc4cf9b74f2..68962081035 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -24,9 +24,16 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, + PRESSURE_BAR, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, + PRESSURE_PSI, ) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util +import homeassistant.util.pressure as pressure_util from . import DOMAIN @@ -51,6 +58,14 @@ UNIT_CONVERSIONS = { POWER_WATT: lambda x: x, POWER_KILO_WATT: lambda x: x * 1000, }, + DEVICE_CLASS_PRESSURE: { + PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR], + PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA], + PRESSURE_INHG: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_INHG], + PRESSURE_MBAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_MBAR], + PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], + PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], + }, } WARN_UNSUPPORTED_UNIT = set() diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 22ad86a6896..24ad3242921 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -32,7 +32,7 @@ def convert(value: float, unit_1: str, unit_2: str) -> float: if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - if unit_1 == unit_2 or unit_1 not in VALID_UNITS: + if unit_1 == unit_2: return value pascals = value / UNIT_CONVERSION[unit_1] From f772eab7b77c70919ef2cfb335fcf1bf17a7a53c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 30 Jun 2021 00:06:24 +0200 Subject: [PATCH 671/750] ESPHome delete store data when unloading entry (#52296) --- homeassistant/components/esphome/__init__.py | 92 +++++++++++++++---- homeassistant/components/esphome/camera.py | 4 +- .../components/esphome/config_flow.py | 7 +- tests/components/esphome/test_config_flow.py | 5 +- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index b91197b019c..aa7da100505 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass, field import functools import logging import math @@ -54,10 +55,55 @@ _T = TypeVar("_T") STORAGE_VERSION = 1 +@dataclass +class DomainData: + """Define a class that stores global esphome data in hass.data[DOMAIN].""" + + _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) + _stores: dict[str, Store] = field(default_factory=dict) + + def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Return the runtime entry data associated with this config entry. + + Raises KeyError if the entry isn't loaded yet. + """ + return self._entry_datas[entry.entry_id] + + def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: + """Set the runtime entry data associated with this config entry.""" + if entry.entry_id in self._entry_datas: + raise ValueError("Entry data for this entry is already set") + self._entry_datas[entry.entry_id] = entry_data + + def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Pop the runtime entry data instance associated with this config entry.""" + return self._entry_datas.pop(entry.entry_id) + + def is_entry_loaded(self, entry: ConfigEntry) -> bool: + """Check whether the given entry is loaded.""" + return entry.entry_id in self._entry_datas + + def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store: + """Get or create a Store instance for the given config entry.""" + return self._stores.setdefault( + entry.entry_id, + Store( + hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder + ), + ) + + @classmethod + def get(cls: type[_T], hass: HomeAssistant) -> _T: + """Get the global DomainData instance stored in hass.data.""" + # Don't use setdefault - this is a hot code path + if DOMAIN in hass.data: + return hass.data[DOMAIN] + ret = hass.data[DOMAIN] = cls() + return ret + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" - hass.data.setdefault(DOMAIN, {}) - host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] @@ -74,13 +120,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zeroconf_instance=zeroconf_instance, ) - # Store client in per-config-entry hass.data - store = Store( - hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder - ) - entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData( - client=cli, entry_id=entry.entry_id, store=store + domain_data = DomainData.get(hass) + entry_data = RuntimeEntryData( + client=cli, + entry_id=entry.entry_id, + store=domain_data.get_or_create_store(hass, entry), ) + domain_data.set_entry_data(entry, entry_data) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" @@ -286,7 +332,11 @@ class ReconnectLogic(RecordUpdateListener): @property def _entry_data(self) -> RuntimeEntryData | None: - return self._hass.data[DOMAIN].get(self._entry.entry_id) + domain_data = DomainData.get(self._hass) + try: + return domain_data.get_entry_data(self._entry) + except KeyError: + return None async def _on_disconnect(self): """Log and issue callbacks when disconnecting.""" @@ -382,7 +432,7 @@ class ReconnectLogic(RecordUpdateListener): return False # Check if the entry got removed or disabled, in which case we shouldn't reconnect - if self._entry.entry_id not in self._hass.data[DOMAIN]: + if not DomainData.get(self._hass).is_entry_loaded(self._entry): # When removing/disconnecting manually return @@ -615,7 +665,8 @@ async def _cleanup_instance( hass: HomeAssistant, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id) + domain_data = DomainData.get(hass) + data = domain_data.pop_entry_data(entry) for disconnect_cb in data.disconnect_callbacks: disconnect_cb() for cleanup_callback in data.cleanup_callbacks: @@ -632,6 +683,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove an esphome config entry.""" + await DomainData.get(hass).get_or_create_store(hass, entry).async_remove() + + async def platform_async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -647,7 +703,7 @@ async def platform_async_setup_entry( This method is in charge of receiving, distributing and storing info and state updates. """ - entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] + entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[component_key] = {} entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @@ -668,7 +724,7 @@ async def platform_async_setup_entry( old_infos.pop(info.key) else: # Create new entity - entity = entity_type(entry.entry_id, component_key, info.key) + entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info @@ -746,9 +802,11 @@ class EsphomeEnumMapper(Generic[_T]): class EsphomeBaseEntity(Entity): """Define a base esphome entity.""" - def __init__(self, entry_id: str, component_key: str, key: int) -> None: + def __init__( + self, entry_data: RuntimeEntryData, component_key: str, key: int + ) -> None: """Initialize.""" - self._entry_id = entry_id + self._entry_data = entry_data self._component_key = component_key self._key = key @@ -784,8 +842,8 @@ class EsphomeBaseEntity(Entity): self.async_write_ha_state() @property - def _entry_data(self) -> RuntimeEntryData: - return self.hass.data[DOMAIN][self._entry_id] + def _entry_id(self) -> str: + return self._entry_data.entry_id @property def _api_version(self) -> APIVersion: diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 6b553de1a13..7afd89bf9be 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -32,10 +32,10 @@ async def async_setup_entry( class EsphomeCamera(Camera, EsphomeBaseEntity): """A camera implementation for ESPHome.""" - def __init__(self, entry_id: str, component_key: str, key: int) -> None: + def __init__(self, *args, **kwargs) -> None: """Initialize.""" Camera.__init__(self) - EsphomeBaseEntity.__init__(self, entry_id, component_key, key) + EsphomeBaseEntity.__init__(self, *args, **kwargs) self._image_cond = asyncio.Condition() @property diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index e31fa202a39..38e44b12508 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -12,8 +12,7 @@ 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 . import DOMAIN -from .entry_data import RuntimeEntryData +from . import DOMAIN, DomainData class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -104,9 +103,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ]: # Is this address or IP address already configured? already_configured = True - elif entry.entry_id in self.hass.data.get(DOMAIN, {}): + elif DomainData.get(self.hass).is_entry_loaded(entry): # Does a config entry with this name already exist? - data: RuntimeEntryData = self.hass.data[DOMAIN][entry.entry_id] + data = DomainData.get(self.hass).get_entry_data(entry) # Node names are unique in the network if data.device_info is not None: diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index d5968e7f731..735a02e960c 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant import config_entries -from homeassistant.components.esphome import DOMAIN +from homeassistant.components.esphome import DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -265,7 +265,8 @@ async def test_discovery_already_configured_name(hass, mock_client): mock_entry_data = MagicMock() mock_entry_data.device_info.name = "test8266" - hass.data[DOMAIN] = {entry.entry_id: mock_entry_data} + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) service_info = { "host": "192.168.43.184", From 04d8f8826927a75c47c059bdfb0108a1cc161e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 30 Jun 2021 01:31:56 +0200 Subject: [PATCH 672/750] Fix Mill consumption data (#52320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/climate.py | 2 +- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 45b628a772c..2d591c67668 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -110,7 +110,7 @@ class MillHeater(ClimateEntity): "controlled_by_tibber": self._heater.tibber_control, "heater_generation": 1 if self._heater.is_gen1 else 2, "consumption_today": self._heater.day_consumption, - "consumption_total": self._heater.total_consumption, + "consumption_total": self._heater.year_consumption, } if self._heater.room: res["room"] = self._heater.room.name diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index f41f03b66c0..161bbe274ef 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.4.1"], + "requirements": ["millheater==0.5.0"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 60af0b35851..7cc9d595407 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,7 +973,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.4.1 +millheater==0.5.0 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cad87b46a2c..97d44a649f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -545,7 +545,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.4.1 +millheater==0.5.0 # homeassistant.components.minio minio==4.0.9 From 5baaede85b3b9cba317b8e4cae520de7a7602650 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 30 Jun 2021 00:11:18 +0000 Subject: [PATCH 673/750] [ci skip] Translation update --- .../cloudflare/translations/nl.json | 7 ++++ .../cloudflare/translations/no.json | 7 ++++ .../cloudflare/translations/zh-Hant.json | 7 ++++ .../components/coinbase/translations/de.json | 40 +++++++++++++++++++ .../components/coinbase/translations/en.json | 4 +- .../components/coinbase/translations/et.json | 2 +- .../components/coinbase/translations/nl.json | 40 +++++++++++++++++++ .../components/coinbase/translations/no.json | 40 +++++++++++++++++++ .../components/coinbase/translations/ru.json | 2 +- .../coinbase/translations/zh-Hant.json | 40 +++++++++++++++++++ .../devolo_home_control/translations/de.json | 6 ++- .../devolo_home_control/translations/nl.json | 6 ++- .../devolo_home_control/translations/no.json | 6 ++- .../translations/zh-Hant.json | 6 ++- .../components/dsmr/translations/nl.json | 1 + .../forecast_solar/translations/no.json | 31 ++++++++++++++ .../nmap_tracker/translations/de.json | 20 ++++++++++ .../nmap_tracker/translations/nl.json | 18 +++++++++ .../nmap_tracker/translations/no.json | 38 ++++++++++++++++++ .../nmap_tracker/translations/zh-Hant.json | 38 ++++++++++++++++++ .../components/onvif/translations/de.json | 13 ++++++ .../components/onvif/translations/nl.json | 13 ++++++ .../components/onvif/translations/no.json | 13 ++++++ .../onvif/translations/zh-Hant.json | 13 ++++++ .../philips_js/translations/nl.json | 9 +++++ .../philips_js/translations/no.json | 9 +++++ .../philips_js/translations/zh-Hant.json | 9 +++++ 27 files changed, 427 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/coinbase/translations/de.json create mode 100644 homeassistant/components/coinbase/translations/nl.json create mode 100644 homeassistant/components/coinbase/translations/no.json create mode 100644 homeassistant/components/coinbase/translations/zh-Hant.json create mode 100644 homeassistant/components/forecast_solar/translations/no.json create mode 100644 homeassistant/components/nmap_tracker/translations/de.json create mode 100644 homeassistant/components/nmap_tracker/translations/nl.json create mode 100644 homeassistant/components/nmap_tracker/translations/no.json create mode 100644 homeassistant/components/nmap_tracker/translations/zh-Hant.json diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json index 7095ff49983..517743be9aa 100644 --- a/homeassistant/components/cloudflare/translations/nl.json +++ b/homeassistant/components/cloudflare/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", "unknown": "Onverwachte fout" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API-token", + "description": "Verifieer opnieuw met uw Cloudflare-account." + } + }, "records": { "data": { "records": "Records" diff --git a/homeassistant/components/cloudflare/translations/no.json b/homeassistant/components/cloudflare/translations/no.json index 4c99154a4e3..1329429474a 100644 --- a/homeassistant/components/cloudflare/translations/no.json +++ b/homeassistant/components/cloudflare/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "unknown": "Uventet feil" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API-token", + "description": "Autentiser p\u00e5 nytt med Cloudflare-kontoen din." + } + }, "records": { "data": { "records": "Poster" diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index da11b44ea8e..3ee29277296 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -11,6 +12,12 @@ }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "API \u6b0a\u6756", + "description": "\u91cd\u65b0\u8a8d\u8b49 Cloudflare \u5e33\u865f\u3002" + } + }, "records": { "data": { "records": "\u8a18\u9304" diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json new file mode 100644 index 00000000000..37ccdedd81a --- /dev/null +++ b/homeassistant/components/coinbase/translations/de.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "api_token": "API-Geheimnis", + "currencies": "Kontostand W\u00e4hrungen", + "exchange_rates": "Wechselkurse" + }, + "description": "Bitte gib die Details Ihres API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt. Trenne mehrere W\u00e4hrungen mit einem Komma (z. B. \"BTC, EUR\")", + "title": "Coinbase API Schl\u00fcssel Details" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von Ihrer Coinbase-API nicht bereitgestellt.", + "exchange_rate_unavaliable": "Einer oder mehrere der angeforderten Wechselkurse werden nicht von Coinbase bereitgestellt.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Zu meldende Wallet-Guthaben.", + "exchange_rate_currencies": "Zu meldende Wechselkurse." + }, + "description": "Coinbase-Optionen anpassen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index fa90f5d82b7..12db6bf8a30 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -12,7 +12,9 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret" + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index 7b58288d98a..ce5ea46ce34 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -16,7 +16,7 @@ "currencies": "Konto saldo valuutad", "exchange_rates": "Vahetuskursid" }, - "description": "Sisesta Coinbase'i pakutavad API-v\u00f5tme \u00fcksikasjad. Eralda mitu valuutat komaga (nt \"BTC, EUR\")", + "description": "Sisesta Coinbase'i pakutava API-v\u00f5tme \u00fcksikasjad.", "title": "Coinbase'i API v\u00f5tme \u00fcksikasjad" } } diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json new file mode 100644 index 00000000000..052caf2f358 --- /dev/null +++ b/homeassistant/components/coinbase/translations/nl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "api_token": "API-geheim", + "currencies": "Valuta's van rekeningsaldo", + "exchange_rates": "Wisselkoersen" + }, + "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase. Scheidt meerdere valuta's met een komma (bijv. \"BTC, EUR\")", + "title": "Coinbase API Sleutel Details" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Een of meer van de gevraagde valutasaldi worden niet geleverd door uw Coinbase API.", + "exchange_rate_unavaliable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase verstrekt.", + "unknown": "Onverwachte fout" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Wallet-saldi om te rapporteren.", + "exchange_rate_currencies": "Wisselkoersen om te rapporteren." + }, + "description": "Coinbase-opties aanpassen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json new file mode 100644 index 00000000000..265ea29e01c --- /dev/null +++ b/homeassistant/components/coinbase/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "api_token": "API-hemmelighet", + "currencies": "Valutaer for kontosaldo", + "exchange_rates": "Valutakurser" + }, + "description": "Vennligst skriv inn detaljene for API-n\u00f8kkelen din som gitt av Coinbase. Skill flere valutaer med komma (f.eks. \"BTC, EUR\")", + "title": "Detaljer for Coinbase API-n\u00f8kkel" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", + "exchange_rate_unavaliable": "En eller flere av de forespurte valutakursene leveres ikke av Coinbase.", + "unknown": "Uventet feil" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Lommeboksaldoer som skal rapporteres.", + "exchange_rate_currencies": "Valutakurser som skal rapporteres." + }, + "description": "Juster Coinbase-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 1d14f56389b..93bb203d24b 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -16,7 +16,7 @@ "currencies": "\u041e\u0441\u0442\u0430\u0442\u043e\u043a \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0430 \u0441\u0447\u0435\u0442\u0435", "exchange_rates": "\u041e\u0431\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u0443\u0440\u0441\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0435 Coinbase. \u0420\u0430\u0437\u0434\u0435\u043b\u044f\u0439\u0442\u0435 \u0432\u0430\u043b\u044e\u0442\u044b \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \"BTC, EUR\").", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API Coinbase.", "title": "\u041a\u043b\u044e\u0447 API Coinbase" } } diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json new file mode 100644 index 00000000000..aa00e459591 --- /dev/null +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "api_token": "API \u5bc6\u9470", + "currencies": "\u5e33\u6236\u9918\u984d\u8ca8\u5e63", + "exchange_rates": "\u532f\u7387" + }, + "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u9470\u8cc7\u8a0a\u3002\u4ee5\u9017\u865f\u5206\u9694\u591a\u7a2e\u8ca8\u5e63\uff08\u4f8b\u5982 \"BTC, EUR\"\uff09", + "title": "Coinbase API \u5bc6\u9470\u8cc7\u6599" + } + } + }, + "options": { + "error": { + "currency_unavaliable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002", + "exchange_rate_unavaliable": "Coinbase \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u532f\u7387\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002", + "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002" + }, + "description": "\u8abf\u6574 Coinbase \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index c34ecdb5c34..4208d80eaec 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_failed": "Bitte verwende denselben mydevolo-Benutzer wie zuvor." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index 0ca81ba7911..c85c685597d 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "reauth_failed": "Gelieve dezelfde mydevolo-gebruiker te gebruiken als voorheen." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 984d279257e..1f1ee69ae47 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "reauth_failed": "Bruk samme mydevolo-bruker som f\u00f8r." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json index 48aa0a9be2e..3bdd499bdbd 100644 --- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_failed": "\u8acb\u4f7f\u7528\u8207\u5148\u524d\u76f8\u540c\u7684 mydevolo \u4f7f\u7528\u8005\u3002" }, "step": { "user": { diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json index 49add2c0e13..ed6171279f1 100644 --- a/homeassistant/components/dsmr/translations/nl.json +++ b/homeassistant/components/dsmr/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "cannot_communicate": "Kon niet verbinden.", "cannot_connect": "Kan geen verbinding maken" }, "error": { diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json new file mode 100644 index 00000000000..5ee0691ecda --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360 grader, 0 = Nord, 90 = \u00d8st, 180 = S\u00f8r, 270 = Vest)", + "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "modules power": "Total Watt-toppeffekt i solcellemodulene dine", + "name": "Navn" + }, + "description": "Fyll ut dataene til solcellepanelene. Se dokumentasjonen hvis et felt er uklart." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API-n\u00f8kkel (valgfritt)", + "azimuth": "Azimut (360 grader, 0 = Nord, 90 = \u00d8st, 180 = S\u00f8r, 270 = Vest)", + "damping": "Dempingsfaktor: justerer resultatene om morgenen og kvelden", + "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)", + "modules power": "Total Watt-toppeffekt i solcellemodulene dine" + }, + "description": "Disse verdiene tillater justering av Solar.Forecast-resultatet. Se dokumentasjonen er et felt som er uklart." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json new file mode 100644 index 00000000000..67c7e8dbd8a --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + } + }, + "options": { + "step": { + "init": { + "data": { + "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", + "hosts": "Zu scannende Netzwerkadressen (kommagetrennt)", + "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" + }, + "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json new file mode 100644 index 00000000000..9f52a6b9add --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "invalid_hosts": "Ongeldige hosts" + }, + "step": { + "user": { + "data": { + "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen" + } + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/no.json b/homeassistant/components/nmap_tracker/translations/no.json new file mode 100644 index 00000000000..487d15c910f --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "invalid_hosts": "Ugyldige verter" + }, + "step": { + "user": { + "data": { + "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", + "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", + "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", + "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" + }, + "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Ugyldige verter" + }, + "step": { + "init": { + "data": { + "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", + "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", + "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", + "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" + }, + "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." + } + } + }, + "title": "Sporing av Nmap" +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hant.json b/homeassistant/components/nmap_tracker/translations/zh-Hant.json new file mode 100644 index 00000000000..ce06358efc7 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_hosts": "\u4e3b\u6a5f\u7aef\u7121\u6548" + }, + "step": { + "user": { + "data": { + "exclude": "\u6392\u9664\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", + "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09", + "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", + "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805" + }, + "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002" + } + } + }, + "options": { + "error": { + "invalid_hosts": "\u4e3b\u6a5f\u7aef\u7121\u6548" + }, + "step": { + "init": { + "data": { + "exclude": "\u6392\u9664\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", + "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09", + "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", + "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805" + }, + "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002" + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 5289f6479cc..109e6256791 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -18,6 +18,16 @@ }, "title": "Konfigurieren Sie die Authentifizierung" }, + "configure": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + }, + "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" + }, "configure_profile": { "data": { "include": "Kameraentit\u00e4t erstellen" @@ -40,6 +50,9 @@ "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" }, "user": { + "data": { + "auto": "Automatisch suchen" + }, "description": "Wenn Sie auf Senden klicken, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stellen Sie sicher, dass ONVIF in der Konfiguration Ihrer Kamera aktiviert ist.", "title": "ONVIF-Ger\u00e4tekonfiguration" } diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index e1fdac8256e..c48d2764672 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -18,6 +18,16 @@ }, "title": "Configureer authenticatie" }, + "configure": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "title": "Configureer ONVIF-apparaat" + }, "configure_profile": { "data": { "include": "Cameraentiteit maken" @@ -40,6 +50,9 @@ "title": "Configureer ONVIF-apparaat" }, "user": { + "data": { + "auto": "Automatisch zoeken" + }, "description": "Door op verzenden te klikken, zoeken we in uw netwerk naar ONVIF-apparaten die Profiel S ondersteunen. \n\nSommige fabrikanten zijn begonnen ONVIF standaard uit te schakelen. Zorg ervoor dat ONVIF is ingeschakeld in de configuratie van uw camera.", "title": "ONVIF-apparaat instellen" } diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json index e30f4e4e909..323f9aba5fe 100644 --- a/homeassistant/components/onvif/translations/no.json +++ b/homeassistant/components/onvif/translations/no.json @@ -18,6 +18,16 @@ }, "title": "Konfigurere godkjenning" }, + "configure": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Konfigurere ONVIF-enhet" + }, "configure_profile": { "data": { "include": "Lag kameraentitet" @@ -40,6 +50,9 @@ "title": "Konfigurere ONVIF-enhet" }, "user": { + "data": { + "auto": "S\u00f8k automatisk" + }, "description": "Ved \u00e5 klikke send inn, vil vi s\u00f8ke nettverket etter ONVIF-enheter som st\u00f8tter Profil S.\n\nNoen produsenter har begynt \u00e5 deaktivere ONVIF som standard. Vennligst kontroller at ONVIF er aktivert i kameraets konfigurasjon.", "title": "ONVIF enhetsoppsett" } diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index 9450b3e9569..fa4d7d632da 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -18,6 +18,16 @@ }, "title": "\u8a2d\u5b9a\u9a57\u8b49" }, + "configure": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e" + }, "configure_profile": { "data": { "include": "\u65b0\u589e\u651d\u5f71\u6a5f\u5be6\u9ad4" @@ -40,6 +50,9 @@ "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e" }, "user": { + "data": { + "auto": "\u81ea\u52d5\u641c\u5c0b" + }, "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u88dd\u7f6e\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 \u88dd\u7f6e\u8a2d\u5b9a" } diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json index 34497d285fa..0b172e24f56 100644 --- a/homeassistant/components/philips_js/translations/nl.json +++ b/homeassistant/components/philips_js/translations/nl.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Apparaat wordt gevraagd om in te schakelen" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Sta het gebruik van de gegevensnotificatieservice toe." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json index 5b1df7c8e8b..732749bcba0 100644 --- a/homeassistant/components/philips_js/translations/no.json +++ b/homeassistant/components/philips_js/translations/no.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "Enheten blir bedt om \u00e5 sl\u00e5 p\u00e5" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Tillat bruk av datavarslingstjeneste." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json index de7f02b7a21..56509ac44dd 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -29,5 +29,14 @@ "trigger_type": { "turn_on": "\u88dd\u7f6e\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "\u5141\u8a31\u4f7f\u7528\u6578\u64da\u9032\u884c\u901a\u77e5\u670d\u52d9\u3002" + } + } + } } } \ No newline at end of file From 3c20f2dd425e135200fd2b634223dd181fb0afde Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 30 Jun 2021 03:10:25 +0200 Subject: [PATCH 674/750] Fix point ConnectionTimeout during startup (#52322) --- homeassistant/components/point/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 43f489d5132..303282ead54 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from httpx import ConnectTimeout from pypoint import PointSession import voluptuous as vol @@ -14,6 +15,7 @@ from homeassistant.const import ( CONF_WEBHOOK_ID, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -92,6 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await session.ensure_active_token() + except ConnectTimeout as err: + _LOGGER.debug("Connection Timeout") + raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except _LOGGER.error("Authentication Error") return False From 9f16e390f514b46b1a9a6c49739ac18c3fa06c32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Jun 2021 17:13:31 -1000 Subject: [PATCH 675/750] Deprecate IPv6 zeroconf setting in favor of the network integration (#51173) --- homeassistant/components/zeroconf/__init__.py | 79 ++++--- .../components/zeroconf/manifest.json | 2 +- homeassistant/components/zeroconf/models.py | 42 ++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zeroconf/conftest.py | 15 ++ tests/components/zeroconf/test_init.py | 220 +++++++++--------- tests/components/zeroconf/test_usage.py | 8 +- 9 files changed, 195 insertions(+), 177 deletions(-) create mode 100644 tests/components/zeroconf/conftest.py diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 64d631e2850..d8d664b63c5 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -15,10 +15,9 @@ from zeroconf import ( InterfaceChoice, IPVersion, NonUniqueNameException, - ServiceInfo, ServiceStateChange, - Zeroconf, ) +from zeroconf.asyncio import AsyncServiceInfo from homeassistant import config_entries, util from homeassistant.components import network @@ -35,7 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass -from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf +from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf from .usage import install_multiple_zeroconf_catcher _LOGGER = logging.getLogger(__name__) @@ -70,6 +69,7 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.deprecated(CONF_DEFAULT_INTERFACE), + cv.deprecated(CONF_IPV6), vol.Schema( { vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, @@ -119,16 +119,16 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero logging.getLogger("zeroconf").setLevel(logging.NOTSET) - aio_zc = HaAsyncZeroconf(**zcargs) - zeroconf = cast(HaZeroconf, aio_zc.zeroconf) + zeroconf = HaZeroconf(**zcargs) + aio_zc = HaAsyncZeroconf(zc=zeroconf) install_multiple_zeroconf_catcher(zeroconf) - def _stop_zeroconf(_event: Event) -> None: + async def _async_stop_zeroconf(_event: Event) -> None: """Stop Zeroconf.""" - zeroconf.ha_close() + await aio_zc.ha_async_close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_zeroconf) hass.data[DOMAIN] = aio_zc return aio_zc @@ -143,7 +143,6 @@ def _async_use_default_interface(adapters: list[Adapter]) -> bool: async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" - zc_config = config.get(DOMAIN, {}) zc_args: dict = {} adapters = await network.async_get_adapters(hass) @@ -158,16 +157,18 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: interfaces.append(ipv4s[0]["address"]) elif ipv6s := adapter["ipv6"]: interfaces.append(ipv6s[0]["scope_id"]) - if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): + + ipv6 = True + if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters): + ipv6 = False zc_args["ip_version"] = IPVersion.V4Only aio_zc = await _async_get_instance(hass, **zc_args) - zeroconf = aio_zc.zeroconf - + zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf_types, homekit_models = await asyncio.gather( async_get_zeroconf(hass), async_get_homekit(hass) ) - discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models) + discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models, ipv6) await discovery.async_setup() async def _async_zeroconf_hass_start(_event: Event) -> None: @@ -230,7 +231,7 @@ async def _async_register_hass_zc_service( _suppress_invalid_properties(params) - info = ServiceInfo( + info = AsyncServiceInfo( ZEROCONF_TYPE, name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", @@ -268,10 +269,10 @@ class FlowDispatcher: self.hass.async_create_task(self._init_flow(flow)) self.pending_flows = [] - def create(self, flow: ZeroconfFlow) -> None: + def async_create(self, flow: ZeroconfFlow) -> None: """Create and add or queue a flow.""" if self.started: - self.hass.create_task(self._init_flow(flow)) + self.hass.async_create_task(self._init_flow(flow)) else: self.pending_flows.append(flow) @@ -288,18 +289,20 @@ class ZeroconfDiscovery: def __init__( self, hass: HomeAssistant, - zeroconf: Zeroconf, + zeroconf: HaZeroconf, zeroconf_types: dict[str, list[dict[str, str]]], homekit_models: dict[str, str], + ipv6: bool, ) -> None: """Init discovery.""" self.hass = hass self.zeroconf = zeroconf self.zeroconf_types = zeroconf_types self.homekit_models = homekit_models + self.ipv6 = ipv6 self.flow_dispatcher: FlowDispatcher | None = None - self.service_browser: HaServiceBrowser | None = None + self.async_service_browser: HaAsyncServiceBrowser | None = None async def async_setup(self) -> None: """Start discovery.""" @@ -311,15 +314,15 @@ class ZeroconfDiscovery: for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES): if hk_type not in self.zeroconf_types: types.append(hk_type) - _LOGGER.debug("Starting Zeroconf browser") - self.service_browser = HaServiceBrowser( - self.zeroconf, types, handlers=[self.service_update] + _LOGGER.debug("Starting Zeroconf browser for: %s", types) + self.async_service_browser = HaAsyncServiceBrowser( + self.ipv6, self.zeroconf, types, handlers=[self.async_service_update] ) async def async_stop(self) -> None: """Cancel the service browser and stop processing the queue.""" - if self.service_browser: - await self.hass.async_add_executor_job(self.service_browser.cancel) + if self.async_service_browser: + await self.async_service_browser.async_cancel() @callback def async_start(self) -> None: @@ -327,21 +330,35 @@ class ZeroconfDiscovery: assert self.flow_dispatcher is not None self.flow_dispatcher.async_start() - def service_update( + @callback + def async_service_update( self, - zeroconf: Zeroconf, + zeroconf: HaZeroconf, service_type: str, name: str, state_change: ServiceStateChange, ) -> None: """Service state changed.""" + _LOGGER.debug( + "service_update: type=%s name=%s state_change=%s", + service_type, + name, + state_change, + ) + if state_change == ServiceStateChange.Removed: return - service_info = ServiceInfo(service_type, name) - service_info.load_from_cache(zeroconf) + asyncio.create_task(self._process_service_update(zeroconf, service_type, name)) - info = info_from_service(service_info) + async def _process_service_update( + self, zeroconf: HaZeroconf, service_type: str, name: str + ) -> None: + """Process a zeroconf update.""" + async_service_info = AsyncServiceInfo(service_type, name) + await async_service_info.async_request(zeroconf, 3000) + + info = info_from_service(async_service_info) if not info: # Prevent the browser thread from collapsing _LOGGER.debug("Failed to get addresses for device %s", name) @@ -353,7 +370,7 @@ class ZeroconfDiscovery: # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: if pending_flow := handle_homekit(self.hass, self.homekit_models, info): - self.flow_dispatcher.create(pending_flow) + self.flow_dispatcher.async_create(pending_flow) # Continue on here as homekit_controller # still needs to get updates on devices # so it can see when the 'c#' field is updated. @@ -415,7 +432,7 @@ class ZeroconfDiscovery: "context": {"source": config_entries.SOURCE_ZEROCONF}, "data": info, } - self.flow_dispatcher.create(flow) + self.flow_dispatcher.async_create(flow) def handle_homekit( @@ -453,7 +470,7 @@ def handle_homekit( return None -def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: +def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: """Return prepared info from mDNS entries.""" properties: dict[str, Any] = {"_raw": {}} diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 030a970d77d..0f7d18446ee 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.31.0"], + "requirements": ["zeroconf==0.32.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py index 5a59ba52e3f..ffa5e1a2ecf 100644 --- a/homeassistant/components/zeroconf/models.py +++ b/homeassistant/components/zeroconf/models.py @@ -1,10 +1,11 @@ """Models for Zeroconf.""" -import asyncio from typing import Any -from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf -from zeroconf.asyncio import AsyncZeroconf +from zeroconf import DNSAddress, DNSRecord, Zeroconf +from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf + +TYPE_AAAA = 28 class HaZeroconf(Zeroconf): @@ -19,33 +20,26 @@ class HaZeroconf(Zeroconf): class HaAsyncZeroconf(AsyncZeroconf): """Home Assistant version of AsyncZeroconf.""" - def __init__( # pylint: disable=super-init-not-called - self, *args: Any, **kwargs: Any - ) -> None: - """Wrap AsyncZeroconf.""" - self.zeroconf = HaZeroconf(*args, **kwargs) - self.loop = asyncio.get_running_loop() - async def async_close(self) -> None: """Fake method to avoid integrations closing it.""" + ha_async_close = AsyncZeroconf.async_close -class HaServiceBrowser(ServiceBrowser): + +class HaAsyncServiceBrowser(AsyncServiceBrowser): """ServiceBrowser that only consumes DNSPointer records.""" - def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: - """Pre-Filter update_record to DNSPointers for the configured type.""" + def __init__(self, ipv6: bool, *args: Any, **kwargs: Any) -> None: + """Create service browser that filters ipv6 if it is disabled.""" + self.ipv6 = ipv6 + super().__init__(*args, **kwargs) - # - # Each ServerBrowser currently runs in its own thread which - # processes every A or AAAA record update per instance. - # - # As the list of zeroconf names we watch for grows, each additional - # ServiceBrowser would process all the A and AAAA updates on the network. - # - # To avoid overwhelming the system we pre-filter here and only process - # DNSPointers for the configured record name (type) - # - if record.name not in self.types or not isinstance(record, DNSPointer): + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: + """Pre-Filter AAAA records if IPv6 is not enabled.""" + if ( + not self.ipv6 + and isinstance(record, DNSAddress) + and record.type == TYPE_AAAA + ): return super().update_record(zc, now, record) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9da1bc22f3e..000711f40e1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.31.0 +zeroconf==0.32.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 7cc9d595407..629961a1fa9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2425,7 +2425,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.31.0 +zeroconf==0.32.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97d44a649f3..9247b1bfefd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ yeelight==0.6.3 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.31.0 +zeroconf==0.32.0 # homeassistant.components.zha zha-quirks==0.0.57 diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py new file mode 100644 index 00000000000..5ccd617f84f --- /dev/null +++ b/tests/components/zeroconf/conftest.py @@ -0,0 +1,15 @@ +"""Tests for the Zeroconf component.""" +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_async_zeroconf(): + """Mock AsyncZeroconf.""" + with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc: + zc = mock_aiozc.return_value + zc.async_register_service = AsyncMock() + zc.zeroconf.async_wait_for_start = AsyncMock() + zc.ha_async_close = AsyncMock() + yield zc diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index ef0ab1fda60..68c0785e60b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,7 +1,8 @@ """Test Zeroconf component setup process.""" from unittest.mock import call, patch -from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange +from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange +from zeroconf.asyncio import AsyncServiceInfo from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -24,29 +25,8 @@ PROPERTIES = { HOMEKIT_STATUS_UNPAIRED = b"1" HOMEKIT_STATUS_PAIRED = b"0" -_ROUTE_NO_LOOPBACK = ( - { - "attrs": [ - ("RTA_TABLE", 254), - ("RTA_DST", "224.0.0.251"), - ("RTA_OIF", 4), - ("RTA_PREFSRC", "192.168.1.5"), - ], - }, -) -_ROUTE_LOOPBACK = ( - { - "attrs": [ - ("RTA_TABLE", 254), - ("RTA_DST", "224.0.0.251"), - ("RTA_OIF", 4), - ("RTA_PREFSRC", "127.0.0.1"), - ], - }, -) - -def service_update_mock(zeroconf, services, handlers, *, limit_service=None): +def service_update_mock(ipv6, zeroconf, services, handlers, *, limit_service=None): """Call service update handler.""" for service in services: if limit_service is not None and service != limit_service: @@ -56,7 +36,7 @@ def service_update_mock(zeroconf, services, handlers, *, limit_service=None): def get_service_info_mock(service_type, name): """Return service info for get_service_info.""" - return ServiceInfo( + return AsyncServiceInfo( service_type, name, addresses=[b"\n\x00\x00\x14"], @@ -70,7 +50,7 @@ def get_service_info_mock(service_type, name): def get_service_info_mock_without_an_address(service_type, name): """Return service info for get_service_info without any addresses.""" - return ServiceInfo( + return AsyncServiceInfo( service_type, name, addresses=[], @@ -86,7 +66,7 @@ def get_homekit_info_mock(model, pairing_status): """Return homekit info for get_service_info for an homekit device.""" def mock_homekit_info(service_type, name): - return ServiceInfo( + return AsyncServiceInfo( service_type, name, addresses=[b"\n\x00\x00\x14"], @@ -104,7 +84,7 @@ def get_zeroconf_info_mock(macaddress): """Return info for get_service_info for an zeroconf device.""" def mock_zc_info(service_type, name): - return ServiceInfo( + return AsyncServiceInfo( service_type, name, addresses=[b"\n\x00\x00\x14"], @@ -122,7 +102,7 @@ def get_zeroconf_info_mock_manufacturer(manufacturer): """Return info for get_service_info for an zeroconf device.""" def mock_zc_info(service_type, name): - return ServiceInfo( + return AsyncServiceInfo( service_type, name, addresses=[b"\n\x00\x00\x14"], @@ -136,14 +116,14 @@ def get_zeroconf_info_mock_manufacturer(manufacturer): return mock_zc_info -async def test_setup(hass, mock_zeroconf): +async def test_setup(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -162,13 +142,15 @@ async def test_setup(hass, mock_zeroconf): # Test instance is set. assert "zeroconf" in hass.data - assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf + assert ( + await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf + ) -async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): +async def test_setup_with_overly_long_url_and_name(hass, mock_async_zeroconf, caplog): """Test we still setup with long urls and names.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.get_url", return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value", @@ -177,7 +159,7 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): "location_name", "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", ), patch( - "homeassistant.components.zeroconf.ServiceInfo.request", + "homeassistant.components.zeroconf.AsyncServiceInfo.request", ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -187,12 +169,12 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): assert "German Umlaut" in caplog.text -async def test_setup_with_default_interface(hass, mock_zeroconf): +async def test_setup_with_default_interface(hass, mock_async_zeroconf): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component( @@ -201,30 +183,30 @@ async def test_setup_with_default_interface(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default) + assert mock_async_zeroconf.called_with(interface_choice=InterfaceChoice.Default) -async def test_setup_without_default_interface(hass, mock_zeroconf): +async def test_setup_without_default_interface(hass, mock_async_zeroconf): """Test without default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} ) - assert mock_zeroconf.called_with() + assert mock_async_zeroconf.called_with() -async def test_setup_without_ipv6(hass, mock_zeroconf): +async def test_setup_without_ipv6(hass, mock_async_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component( @@ -233,15 +215,15 @@ async def test_setup_without_ipv6(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(ip_version=IPVersion.V4Only) + assert mock_async_zeroconf.called_with(ip_version=IPVersion.V4Only) -async def test_setup_with_ipv6(hass, mock_zeroconf): +async def test_setup_with_ipv6(hass, mock_async_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component( @@ -250,28 +232,28 @@ async def test_setup_with_ipv6(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with() + assert mock_async_zeroconf.called_with() -async def test_setup_with_ipv6_default(hass, mock_zeroconf): +async def test_setup_with_ipv6_default(hass, mock_async_zeroconf): """Test without ipv6 as default.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with() + assert mock_async_zeroconf.called_with() -async def test_zeroconf_match_macaddress(hass, mock_zeroconf): +async def test_zeroconf_match_macaddress(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" - def http_only_service_update_mock(zeroconf, services, handlers): + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -291,9 +273,9 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -305,10 +287,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "shelly" -async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): +async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" - def http_only_service_update_mock(zeroconf, services, handlers): + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -324,9 +306,9 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -338,10 +320,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" -async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): +async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf): """Test matchers reject when a property is missing.""" - def http_only_service_update_mock(zeroconf, services, handlers): + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -357,9 +339,9 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_zeroconf_info_mock("aabbccddeeff"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -370,10 +352,10 @@ async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == 0 -async def test_zeroconf_no_match(hass, mock_zeroconf): +async def test_zeroconf_no_match(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" - def http_only_service_update_mock(zeroconf, services, handlers): + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -389,9 +371,9 @@ async def test_zeroconf_no_match(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -402,10 +384,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == 0 -async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): +async def test_zeroconf_no_match_manufacturer(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" - def http_only_service_update_mock(zeroconf, services, handlers): + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -421,9 +403,9 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -434,7 +416,7 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == 0 -async def test_homekit_match_partial_space(hass, mock_zeroconf): +async def test_homekit_match_partial_space(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, @@ -444,12 +426,12 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -461,7 +443,7 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "lifx" -async def test_homekit_match_partial_dash(hass, mock_zeroconf): +async def test_homekit_match_partial_dash(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, @@ -471,12 +453,12 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -488,7 +470,7 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "rachio" -async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf): +async def test_homekit_match_partial_fnmatch(hass, mock_async_zeroconf): """Test matching homekit devices with fnmatch.""" with patch.dict( zc_gen.ZEROCONF, @@ -498,12 +480,12 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -515,7 +497,7 @@ async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "yeelight" -async def test_homekit_match_full(hass, mock_zeroconf): +async def test_homekit_match_full(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, @@ -525,12 +507,12 @@ async def test_homekit_match_full(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -542,7 +524,7 @@ async def test_homekit_match_full(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "hue" -async def test_homekit_already_paired(hass, mock_zeroconf): +async def test_homekit_already_paired(hass, mock_async_zeroconf): """Test that an already paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, @@ -552,12 +534,12 @@ async def test_homekit_already_paired(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -570,7 +552,7 @@ async def test_homekit_already_paired(hass, mock_zeroconf): assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" -async def test_homekit_invalid_paring_status(hass, mock_zeroconf): +async def test_homekit_invalid_paring_status(hass, mock_async_zeroconf): """Test that missing paring data is not sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, @@ -580,12 +562,12 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, - "HaServiceBrowser", + "HaAsyncServiceBrowser", side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock("tado", b"invalid"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -597,7 +579,7 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "tado" -async def test_homekit_not_paired(hass, mock_zeroconf): +async def test_homekit_not_paired(hass, mock_async_zeroconf): """Test that an not paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, @@ -606,9 +588,9 @@ async def test_homekit_not_paired(hass, mock_zeroconf): ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_homekit_info_mock( "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED ), @@ -646,19 +628,21 @@ async def test_info_from_service_with_addresses(hass): assert info is None -async def test_get_instance(hass, mock_zeroconf): +async def test_get_instance(hass, mock_async_zeroconf): """Test we get an instance.""" assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf + assert ( + await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf + ) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert len(mock_zeroconf.ha_close.mock_calls) == 1 + assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1 -async def test_removed_ignored(hass, mock_zeroconf): +async def test_removed_ignored(hass, mock_async_zeroconf): """Test we remove it when a zeroconf entry is removed.""" - def service_update_mock(zeroconf, services, handlers): + def service_update_mock(ipv6, zeroconf, services, handlers): """Call service update handler.""" handlers[0]( zeroconf, @@ -680,9 +664,9 @@ async def test_removed_ignored(hass, mock_zeroconf): ) with patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ) as mock_service_info: assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -709,24 +693,28 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [ ] -async def test_async_detect_interfaces_setting_non_loopback_route(hass): +async def test_async_detect_interfaces_setting_non_loopback_route( + hass, mock_async_zeroconf +): """Test without default interface config and the route returns a non-loopback address.""" - with patch( - "homeassistant.components.zeroconf.models.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( + hass.config_entries.flow, "async_init" + ), patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.network.async_get_adapters", return_value=_ADAPTER_WITH_DEFAULT_ENABLED, ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default) + assert mock_zc.mock_calls[0] == call( + interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only + ) _ADAPTERS_WITH_MANUAL_CONFIG = [ @@ -764,17 +752,17 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ ] -async def test_async_detect_interfaces_setting_empty_route(hass): +async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zeroconf): """Test without default interface config and the route returns nothing.""" - with patch( - "homeassistant.components.zeroconf.models.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( + hass.config_entries.flow, "async_init" + ), patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.network.async_get_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, ), patch( - "homeassistant.components.zeroconf.ServiceInfo", + "homeassistant.components.zeroconf.AsyncServiceInfo", side_effect=get_service_info_mock, ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 0f902632db8..40c813b506c 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -10,7 +10,9 @@ from homeassistant.setup import async_setup_component DOMAIN = "zeroconf" -async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog): +async def test_multiple_zeroconf_instances( + hass, mock_async_zeroconf, mock_zeroconf, caplog +): """Test creating multiple zeroconf throws without an integration.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @@ -24,7 +26,9 @@ async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog): assert "Zeroconf" in caplog.text -async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog): +async def test_multiple_zeroconf_instances_gives_shared( + hass, mock_async_zeroconf, mock_zeroconf, caplog +): """Test creating multiple zeroconf gives the shared instance to an integration.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From f2906d0fca1b8e1126b133904a9945da7d8cf68c Mon Sep 17 00:00:00 2001 From: Carlos Gomes <50534116+cgomesu@users.noreply.github.com> Date: Wed, 30 Jun 2021 03:31:33 -0300 Subject: [PATCH 676/750] Add quantiles to Statistics integration (#52189) * Add quantiles as another Statistics attribute Quantiles divide states into intervals of equal probability. The statistics.quantiles() function was added in Python 3.8 and can now be included in the Statistics integration without new dependencies. Quantiles can be used in conjunction with other distribution metrics to create box plots (quartiles) and other graphical resources for visualizing the distribution of states. * Add quantiles reference to basic tests --- homeassistant/components/statistics/sensor.py | 53 +++++++++++++++++-- tests/components/statistics/test_sensor.py | 5 ++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index e32ae0debaf..b1ea6cfb50f 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -39,6 +39,7 @@ ATTR_MEAN = "mean" ATTR_MEDIAN = "median" ATTR_MIN_AGE = "min_age" ATTR_MIN_VALUE = "min_value" +ATTR_QUANTILES = "quantiles" ATTR_SAMPLING_SIZE = "sampling_size" ATTR_STANDARD_DEVIATION = "standard_deviation" ATTR_TOTAL = "total" @@ -47,10 +48,14 @@ ATTR_VARIANCE = "variance" CONF_SAMPLING_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" CONF_PRECISION = "precision" +CONF_QUANTILE_INTERVALS = "quantile_intervals" +CONF_QUANTILE_METHOD = "quantile_method" DEFAULT_NAME = "Stats" DEFAULT_SIZE = 20 DEFAULT_PRECISION = 2 +DEFAULT_QUANTILE_INTERVALS = 4 +DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -62,6 +67,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_MAX_AGE): cv.time_period, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), + vol.Optional( + CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS + ): vol.All(vol.Coerce(int), vol.Range(min=2)), + vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In( + ["exclusive", "inclusive"] + ), } ) @@ -76,9 +87,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sampling_size = config.get(CONF_SAMPLING_SIZE) max_age = config.get(CONF_MAX_AGE) precision = config.get(CONF_PRECISION) + quantile_intervals = config.get(CONF_QUANTILE_INTERVALS) + quantile_method = config.get(CONF_QUANTILE_METHOD) async_add_entities( - [StatisticsSensor(entity_id, name, sampling_size, max_age, precision)], True + [ + StatisticsSensor( + entity_id, + name, + sampling_size, + max_age, + precision, + quantile_intervals, + quantile_method, + ) + ], + True, ) return True @@ -87,7 +111,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" - def __init__(self, entity_id, name, sampling_size, max_age, precision): + def __init__( + self, + entity_id, + name, + sampling_size, + max_age, + precision, + quantile_intervals, + quantile_method, + ): """Initialize the Statistics sensor.""" self._entity_id = entity_id self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" @@ -95,12 +128,14 @@ class StatisticsSensor(SensorEntity): self._sampling_size = sampling_size self._max_age = max_age self._precision = precision + self._quantile_intervals = quantile_intervals + self._quantile_method = quantile_method self._unit_of_measurement = None self.states = deque(maxlen=self._sampling_size) self.ages = deque(maxlen=self._sampling_size) self.count = 0 - self.mean = self.median = self.stdev = self.variance = None + self.mean = self.median = self.quantiles = self.stdev = self.variance = None self.total = self.min = self.max = None self.min_age = self.max_age = None self.change = self.average_change = self.change_rate = None @@ -191,6 +226,7 @@ class StatisticsSensor(SensorEntity): ATTR_COUNT: self.count, ATTR_MEAN: self.mean, ATTR_MEDIAN: self.median, + ATTR_QUANTILES: self.quantiles, ATTR_STANDARD_DEVIATION: self.stdev, ATTR_VARIANCE: self.variance, ATTR_TOTAL: self.total, @@ -257,9 +293,18 @@ class StatisticsSensor(SensorEntity): try: # require at least two data points self.stdev = round(statistics.stdev(self.states), self._precision) self.variance = round(statistics.variance(self.states), self._precision) + if self._quantile_intervals < self.count: + self.quantiles = [ + round(quantile, self._precision) + for quantile in statistics.quantiles( + self.states, + n=self._quantile_intervals, + method=self._quantile_method, + ) + ] except statistics.StatisticsError as err: _LOGGER.debug("%s: %s", self.entity_id, err) - self.stdev = self.variance = STATE_UNKNOWN + self.stdev = self.variance = self.quantiles = STATE_UNKNOWN if self.states: self.total = round(sum(self.states), self._precision) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 60de732cf79..bcbf13b8298 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -48,6 +48,9 @@ class TestStatisticsSensor(unittest.TestCase): self.median = round(statistics.median(self.values), 2) self.deviation = round(statistics.stdev(self.values), 2) self.variance = round(statistics.variance(self.values), 2) + self.quantiles = [ + round(quantile, 2) for quantile in statistics.quantiles(self.values) + ] self.change = round(self.values[-1] - self.values[0], 2) self.average_change = round(self.change / (len(self.values) - 1), 2) self.change_rate = round(self.change / (60 * (self.count - 1)), 2) @@ -112,6 +115,7 @@ class TestStatisticsSensor(unittest.TestCase): assert self.variance == state.attributes.get("variance") assert self.median == state.attributes.get("median") assert self.deviation == state.attributes.get("standard_deviation") + assert self.quantiles == state.attributes.get("quantiles") assert self.mean == state.attributes.get("mean") assert self.count == state.attributes.get("count") assert self.total == state.attributes.get("total") @@ -188,6 +192,7 @@ class TestStatisticsSensor(unittest.TestCase): # require at least two data points assert state.attributes.get("variance") == STATE_UNKNOWN assert state.attributes.get("standard_deviation") == STATE_UNKNOWN + assert state.attributes.get("quantiles") == STATE_UNKNOWN def test_max_age(self): """Test value deprecation.""" From da9bb99ba8bfd6d4daa17f9d7a63533cf3cbd4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Betts?= Date: Wed, 30 Jun 2021 08:43:02 +0200 Subject: [PATCH 677/750] Create service to enable Continuous Mode on Nuki Opener (#51861) --- homeassistant/components/nuki/const.py | 1 + homeassistant/components/nuki/lock.py | 18 ++++++++++++++++++ homeassistant/components/nuki/services.yaml | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index da12a3a074d..680454c3edc 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -4,6 +4,7 @@ DOMAIN = "nuki" # Attributes ATTR_BATTERY_CRITICAL = "battery_critical" ATTR_NUKI_ID = "nuki_id" +ATTR_ENABLE = "enable" ATTR_UNLATCH = "unlatch" # Data diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 48e72b88530..9cb1bd01524 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -12,6 +12,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform from . import NukiEntity from .const import ( ATTR_BATTERY_CRITICAL, + ATTR_ENABLE, ATTR_NUKI_ID, ATTR_UNLATCH, DATA_COORDINATOR, @@ -58,6 +59,11 @@ async def async_setup_entry(hass, entry, async_add_entities): vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, }, "lock_n_go", + "set_continuous_mode", + { + vol.Required(ATTR_ENABLE): cv.boolean, + }, + "set_continuous_mode", ) @@ -165,3 +171,15 @@ class NukiOpenerEntity(NukiDeviceEntity): def lock_n_go(self, unlatch): """Stub service.""" + + def set_continuous_mode(self, enable): + """Continuous Mode. + + This feature will cause the door to automatically open when anyone + rings the bell. This is similar to ring-to-open, except that it does + not automatically deactivate + """ + if enable: + self._nuki_device.activate_continuous_mode() + else: + self._nuki_device.deactivate_continuous_mode() diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index d923885efc6..c43f081dbf7 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -12,3 +12,17 @@ lock_n_go: default: false selector: boolean: +set_continuous_mode: + name: Set Continuous Mode + description: "Enable or disable Continuous Mode on Nuki Opener" + target: + entity: + integration: nuki + domain: lock + fields: + enable: + name: Enable + description: Whether to enable or disable the feature + default: false + selector: + boolean: From 3902f0bdd4a14962f771379d09056114ee4e3fac Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Jun 2021 04:33:50 -0500 Subject: [PATCH 678/750] Speed up lookup of AirVisual pollutant labels, levels, and units (#52327) * Speed up lookup of AirVisual pollutant levels, labels, and units * Mispellings --- homeassistant/components/airvisual/sensor.py | 83 ++++++++------------ 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index ec8fe108b7f..c5d6621a329 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -56,57 +56,32 @@ NODE_PRO_SENSORS = [ (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), ] +POLLUTANT_LABELS = { + "co": "Carbon Monoxide", + "n2": "Nitrogen Dioxide", + "o3": "Ozone", + "p1": "PM10", + "p2": "PM2.5", + "s2": "Sulfur Dioxide", +} -@callback -def async_get_pollutant_label(symbol): - """Get a pollutant's label based on its symbol.""" - if symbol == "co": - return "Carbon Monoxide" - if symbol == "n2": - return "Nitrogen Dioxide" - if symbol == "o3": - return "Ozone" - if symbol == "p1": - return "PM10" - if symbol == "p2": - return "PM2.5" - if symbol == "s2": - return "Sulfur Dioxide" - return symbol +POLLUTANT_LEVELS = { + (0, 50): ("Good", "mdi:emoticon-excited"), + (51, 100): ("Moderate", "mdi:emoticon-happy"), + (101, 150): ("Unhealthy for sensitive groups", "mdi:emoticon-neutral"), + (151, 200): ("Unhealthy", "mdi:emoticon-sad"), + (201, 300): ("Very unhealthy", "mdi:emoticon-dead"), + (301, 1000): ("Hazardous", "mdi:biohazard"), +} - -@callback -def async_get_pollutant_level_info(value): - """Return a verbal pollutant level (and associated icon) for a numeric value.""" - if 0 <= value <= 50: - return ("Good", "mdi:emoticon-excited") - if 51 <= value <= 100: - return ("Moderate", "mdi:emoticon-happy") - if 101 <= value <= 150: - return ("Unhealthy for sensitive groups", "mdi:emoticon-neutral") - if 151 <= value <= 200: - return ("Unhealthy", "mdi:emoticon-sad") - if 201 <= value <= 300: - return ("Very Unhealthy", "mdi:emoticon-dead") - return ("Hazardous", "mdi:biohazard") - - -@callback -def async_get_pollutant_unit(symbol): - """Get a pollutant's unit based on its symbol.""" - if symbol == "co": - return CONCENTRATION_PARTS_PER_MILLION - if symbol == "n2": - return CONCENTRATION_PARTS_PER_BILLION - if symbol == "o3": - return CONCENTRATION_PARTS_PER_BILLION - if symbol == "p1": - return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - if symbol == "p2": - return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - if symbol == "s2": - return CONCENTRATION_PARTS_PER_BILLION - return None +POLLUTANT_UNITS = { + "co": CONCENTRATION_PARTS_PER_MILLION, + "n2": CONCENTRATION_PARTS_PER_BILLION, + "o3": CONCENTRATION_PARTS_PER_BILLION, + "p1": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "p2": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "s2": CONCENTRATION_PARTS_PER_BILLION, +} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -197,16 +172,20 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - self._state, self._attr_icon = async_get_pollutant_level_info(aqi) + [(self._state, self._attr_icon)] = [ + (name, icon) + for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items() + if floor <= aqi <= ceiling + ] elif self._kind == SENSOR_KIND_AQI: self._state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: symbol = data[f"main{self._locale}"] - self._state = async_get_pollutant_label(symbol) + self._state = POLLUTANT_LABELS[symbol] self._attrs.update( { ATTR_POLLUTANT_SYMBOL: symbol, - ATTR_POLLUTANT_UNIT: async_get_pollutant_unit(symbol), + ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol], } ) From 3e4dacb885ae4e286bd67fee572859d2bc7b68c8 Mon Sep 17 00:00:00 2001 From: Brian Towles Date: Wed, 30 Jun 2021 04:56:02 -0500 Subject: [PATCH 679/750] Add Modern Forms binary sensor platform (#52312) Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- .../components/modern_forms/__init__.py | 2 + .../components/modern_forms/binary_sensor.py | 114 ++++++++++++++++++ .../modern_forms/test_binary_sensor.py | 46 +++++++ 3 files changed, 162 insertions(+) create mode 100644 homeassistant/components/modern_forms/binary_sensor.py create mode 100644 tests/components/modern_forms/test_binary_sensor.py diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 24195b96ea4..a31b2655184 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -11,6 +11,7 @@ from aiomodernforms import ( ) from aiomodernforms.models import Device as ModernFormsDeviceState +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -30,6 +31,7 @@ from .const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = [ + BINARY_SENSOR_DOMAIN, LIGHT_DOMAIN, FAN_DOMAIN, SENSOR_DOMAIN, diff --git a/homeassistant/components/modern_forms/binary_sensor.py b/homeassistant/components/modern_forms/binary_sensor.py new file mode 100644 index 00000000000..f8e3f8bbcf8 --- /dev/null +++ b/homeassistant/components/modern_forms/binary_sensor.py @@ -0,0 +1,114 @@ +"""Support for Modern Forms Binary Sensors.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util + +from . import ModernFormsDataUpdateCoordinator, ModernFormsDeviceEntity +from .const import CLEAR_TIMER, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Modern Forms binary sensors.""" + coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + binary_sensors: list[ModernFormsBinarySensor] = [ + ModernFormsFanSleepTimerActive(entry.entry_id, coordinator), + ] + + # Only setup light sleep timer sensor if light unit installed + if coordinator.data.info.light_type: + binary_sensors.append( + ModernFormsLightSleepTimerActive(entry.entry_id, coordinator) + ) + + async_add_entities(binary_sensors) + + +class ModernFormsBinarySensor(ModernFormsDeviceEntity, BinarySensorEntity): + """Defines a Modern Forms binary sensor.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: ModernFormsDataUpdateCoordinator, + name: str, + icon: str, + key: str, + ) -> None: + """Initialize Modern Forms switch.""" + super().__init__( + entry_id=entry_id, coordinator=coordinator, name=name, icon=icon + ) + + self._attr_unique_id = f"{coordinator.data.info.mac_address}_{key}" + + +class ModernFormsLightSleepTimerActive(ModernFormsBinarySensor): + """Defines a Modern Forms Light Sleep Timer Active sensor.""" + + _attr_entity_registry_enabled_default = False + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Light Sleep Timer Active sensor.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:av-timer", + key="light_sleep_timer_active", + name=f"{coordinator.data.info.device_name} Light Sleep Timer Active", + ) + + @property + def is_on(self) -> bool: + """Return the state of the timer.""" + return not ( + self.coordinator.data.state.light_sleep_timer == CLEAR_TIMER + or ( + dt_util.utc_from_timestamp( + self.coordinator.data.state.light_sleep_timer + ) + - dt_util.utcnow() + ).total_seconds() + < 0 + ) + + +class ModernFormsFanSleepTimerActive(ModernFormsBinarySensor): + """Defines a Modern Forms Fan Sleep Timer Active sensor.""" + + _attr_entity_registry_enabled_default = False + + def __init__( + self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator + ) -> None: + """Initialize Modern Forms Fan Sleep Timer Active sensor.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:av-timer", + key="fan_sleep_timer_active", + name=f"{coordinator.data.info.device_name} Fan Sleep Timer Active", + ) + + @property + def is_on(self) -> bool: + """Return the state of the timer.""" + return not ( + self.coordinator.data.state.fan_sleep_timer == CLEAR_TIMER + or ( + dt_util.utc_from_timestamp(self.coordinator.data.state.fan_sleep_timer) + - dt_util.utcnow() + ).total_seconds() + < 0 + ) diff --git a/tests/components/modern_forms/test_binary_sensor.py b/tests/components/modern_forms/test_binary_sensor.py new file mode 100644 index 00000000000..bc32e309958 --- /dev/null +++ b/tests/components/modern_forms/test_binary_sensor.py @@ -0,0 +1,46 @@ +"""Tests for the Modern Forms sensor platform.""" +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.modern_forms.const import DOMAIN +from homeassistant.const import ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.modern_forms import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_binary_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Modern Forms sensors.""" + + registry = er.async_get(hass) + + registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + "AA:BB:CC:DD:EE:FF_light_sleep_timer_active", + suggested_object_id="modernformsfan_light_sleep_timer_active", + disabled_by=None, + ) + registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + DOMAIN, + "AA:BB:CC:DD:EE:FF_fan_sleep_timer_active", + suggested_object_id="modernformsfan_fan_sleep_timer_active", + disabled_by=None, + ) + + await init_integration(hass, aioclient_mock) + + # Light timer remaining time + state = hass.states.get("binary_sensor.modernformsfan_light_sleep_timer_active") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:av-timer" + assert state.state == "off" + + # Fan timer remaining time + state = hass.states.get("binary_sensor.modernformsfan_fan_sleep_timer_active") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:av-timer" + assert state.state == "off" From a7ece4ecaa1fa37b223c6e47754cccce08f16764 Mon Sep 17 00:00:00 2001 From: mlemainque Date: Wed, 30 Jun 2021 12:01:08 +0200 Subject: [PATCH 680/750] Fix Daikin integration power sensors (#51905) --- homeassistant/components/daikin/const.py | 12 ++++++++++++ homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/sensor.py | 15 ++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 5b4bdd28331..b03c8eb113d 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -5,10 +5,12 @@ from homeassistant.const import ( CONF_NAME, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, PERCENTAGE, POWER_KILO_WATT, TEMP_CELSIUS, @@ -24,6 +26,7 @@ ATTR_COOL_ENERGY = "cool_energy" ATTR_HEAT_ENERGY = "heat_energy" ATTR_HUMIDITY = "humidity" ATTR_TARGET_HUMIDITY = "target_humidity" +ATTR_COMPRESSOR_FREQUENCY = "compressor_frequency" ATTR_STATE_ON = "on" ATTR_STATE_OFF = "off" @@ -32,6 +35,7 @@ SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_POWER = "power" SENSOR_TYPE_ENERGY = "energy" +SENSOR_TYPE_FREQUENCY = "frequency" SENSOR_TYPES = { ATTR_INSIDE_TEMPERATURE: { @@ -68,14 +72,22 @@ SENSOR_TYPES = { CONF_NAME: "Cool Energy Consumption", CONF_TYPE: SENSOR_TYPE_ENERGY, CONF_ICON: "mdi:snowflake", + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, ATTR_HEAT_ENERGY: { CONF_NAME: "Heat Energy Consumption", CONF_TYPE: SENSOR_TYPE_ENERGY, CONF_ICON: "mdi:fire", + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, + ATTR_COMPRESSOR_FREQUENCY: { + CONF_NAME: "Compressor Frequency", + CONF_TYPE: SENSOR_TYPE_FREQUENCY, + CONF_ICON: "mdi:fan", + CONF_UNIT_OF_MEASUREMENT: FREQUENCY_HERTZ, + }, } CONF_UUID = "uuid" diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 704cfcf739c..f34dc8edc57 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.3"], + "requirements": ["pydaikin==2.4.4"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index a5b515ea918..3bfc0a3926c 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi from .const import ( + ATTR_COMPRESSOR_FREQUENCY, ATTR_COOL_ENERGY, ATTR_HEAT_ENERGY, ATTR_HUMIDITY, @@ -18,6 +19,7 @@ from .const import ( ATTR_TARGET_HUMIDITY, ATTR_TOTAL_POWER, SENSOR_TYPE_ENERGY, + SENSOR_TYPE_FREQUENCY, SENSOR_TYPE_HUMIDITY, SENSOR_TYPE_POWER, SENSOR_TYPE_TEMPERATURE, @@ -46,6 +48,8 @@ async def async_setup_entry(hass, entry, async_add_entities): if daikin_api.device.support_humidity: sensors.append(ATTR_HUMIDITY) sensors.append(ATTR_TARGET_HUMIDITY) + if daikin_api.device.support_compressor_frequency: + sensors.append(ATTR_COMPRESSOR_FREQUENCY) async_add_entities([DaikinSensor.factory(daikin_api, sensor) for sensor in sensors]) @@ -60,6 +64,7 @@ class DaikinSensor(SensorEntity): SENSOR_TYPE_HUMIDITY: DaikinClimateSensor, SENSOR_TYPE_POWER: DaikinPowerSensor, SENSOR_TYPE_ENERGY: DaikinPowerSensor, + SENSOR_TYPE_FREQUENCY: DaikinClimateSensor, }[SENSOR_TYPES[monitored_state][CONF_TYPE]] return cls(api, monitored_state) @@ -125,6 +130,10 @@ class DaikinClimateSensor(DaikinSensor): return self._api.device.humidity if self._device_attribute == ATTR_TARGET_HUMIDITY: return self._api.device.target_humidity + + if self._device_attribute == ATTR_COMPRESSOR_FREQUENCY: + return self._api.device.compressor_frequency + return None @@ -135,9 +144,9 @@ class DaikinPowerSensor(DaikinSensor): def state(self): """Return the state of the sensor.""" if self._device_attribute == ATTR_TOTAL_POWER: - return round(self._api.device.current_total_power_consumption, 3) + return round(self._api.device.current_total_power_consumption, 2) if self._device_attribute == ATTR_COOL_ENERGY: - return round(self._api.device.last_hour_cool_energy_consumption, 3) + return round(self._api.device.last_hour_cool_energy_consumption, 2) if self._device_attribute == ATTR_HEAT_ENERGY: - return round(self._api.device.last_hour_heat_energy_consumption, 3) + return round(self._api.device.last_hour_heat_energy_consumption, 2) return None diff --git a/requirements_all.txt b/requirements_all.txt index 629961a1fa9..df9cd4fb2fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1376,7 +1376,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.3 +pydaikin==2.4.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9247b1bfefd..1659c6f7766 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.3 +pydaikin==2.4.4 # homeassistant.components.deconz pydeconz==80 From dc407fe7a123301adef9c4bbfa1d11eddc355741 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Wed, 30 Jun 2021 13:09:57 +0200 Subject: [PATCH 681/750] Fix MusicCast subwoofers (#52335) --- .../components/yamaha_musiccast/manifest.json | 2 +- .../yamaha_musiccast/media_player.py | 37 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 1ff4e2efdf4..501e3b8a00b 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.6" + "aiomusiccast==0.8.0" ], "ssdp": [ { diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 88033cf68d1..b67aa834008 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -4,12 +4,12 @@ from __future__ import annotations import logging from aiomusiccast import MusicCastGroupException +from aiomusiccast.features import ZoneFeature import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( REPEAT_MODE_OFF, - SUPPORT_CLEAR_PLAYLIST, SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -55,13 +55,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -MUSIC_PLAYER_SUPPORT = ( +MUSIC_PLAYER_BASE_SUPPORT = ( SUPPORT_PAUSE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_CLEAR_PLAYLIST | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_REPEAT_SET @@ -210,13 +205,17 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @property def volume_level(self): """Return the volume level of the media player (0..1).""" - volume = self.coordinator.data.zones[self._zone_id].current_volume - return (volume - self._volume_min) / (self._volume_max - self._volume_min) + if ZoneFeature.VOLUME in self.coordinator.data.zones[self._zone_id].features: + volume = self.coordinator.data.zones[self._zone_id].current_volume + return (volume - self._volume_min) / (self._volume_max - self._volume_min) + return None @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" - return self.coordinator.data.zones[self._zone_id].mute + if ZoneFeature.VOLUME in self.coordinator.data.zones[self._zone_id].features: + return self.coordinator.data.zones[self._zone_id].mute + return None @property def shuffle(self): @@ -350,7 +349,17 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @property def supported_features(self): """Flag media player features that are supported.""" - return MUSIC_PLAYER_SUPPORT + supported_features = MUSIC_PLAYER_BASE_SUPPORT + zone = self.coordinator.data.zones[self._zone_id] + + if ZoneFeature.POWER in zone.features: + supported_features |= SUPPORT_TURN_ON | SUPPORT_TURN_OFF + if ZoneFeature.VOLUME in zone.features: + supported_features |= SUPPORT_VOLUME_SET + if ZoneFeature.MUTE in zone.features: + supported_features |= SUPPORT_VOLUME_MUTE + + return supported_features async def async_media_previous_track(self): """Send previous track command.""" @@ -374,12 +383,6 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): "Service next track is not supported for non NetUSB or Tuner sources." ) - def clear_playlist(self): - """Clear players playlist.""" - self._cur_track = 0 - self._player_state = STATE_OFF - self.async_write_ha_state() - async def async_set_repeat(self, repeat): """Enable/disable repeat mode.""" if self._is_netusb: diff --git a/requirements_all.txt b/requirements_all.txt index df9cd4fb2fd..498e5154c21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,7 +209,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.6 +aiomusiccast==0.8.0 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1659c6f7766..101280ddf3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.6 +aiomusiccast==0.8.0 # homeassistant.components.notion aionotion==1.1.0 From d8337cf98f5b85d6acd582ddfe1edf3138ead568 Mon Sep 17 00:00:00 2001 From: stefano055415 Date: Wed, 30 Jun 2021 13:21:06 +0200 Subject: [PATCH 682/750] Add Freedompro (#46332) Co-authored-by: Milan Meulemans Co-authored-by: Maciej Bieniek Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + .../components/freedompro/__init__.py | 84 +++++++ .../components/freedompro/config_flow.py | 73 ++++++ homeassistant/components/freedompro/const.py | 3 + homeassistant/components/freedompro/light.py | 109 +++++++++ .../components/freedompro/manifest.json | 11 + .../components/freedompro/strings.json | 20 ++ .../freedompro/translations/en.json | 20 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/freedompro/__init__.py | 1 + tests/components/freedompro/conftest.py | 67 ++++++ tests/components/freedompro/const.py | 219 ++++++++++++++++++ .../components/freedompro/test_config_flow.py | 82 +++++++ tests/components/freedompro/test_init.py | 55 +++++ tests/components/freedompro/test_light.py | 155 +++++++++++++ 17 files changed, 907 insertions(+) create mode 100644 homeassistant/components/freedompro/__init__.py create mode 100644 homeassistant/components/freedompro/config_flow.py create mode 100644 homeassistant/components/freedompro/const.py create mode 100644 homeassistant/components/freedompro/light.py create mode 100644 homeassistant/components/freedompro/manifest.json create mode 100644 homeassistant/components/freedompro/strings.json create mode 100644 homeassistant/components/freedompro/translations/en.json create mode 100644 tests/components/freedompro/__init__.py create mode 100644 tests/components/freedompro/conftest.py create mode 100644 tests/components/freedompro/const.py create mode 100644 tests/components/freedompro/test_config_flow.py create mode 100644 tests/components/freedompro/test_init.py create mode 100644 tests/components/freedompro/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 70fbaf8ce1f..46eb14dd66a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame +homeassistant/components/freedompro/* @stefano055415 homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py new file mode 100644 index 00000000000..47c0bda1b1c --- /dev/null +++ b/homeassistant/components/freedompro/__init__.py @@ -0,0 +1,84 @@ +"""Support for freedompro.""" +from datetime import timedelta +import logging + +from pyfreedompro import get_list, get_states + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["light"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Freedompro from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + api_key = entry.data[CONF_API_KEY] + + coordinator = FreedomproDataUpdateCoordinator(hass, api_key) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Update listener.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +class FreedomproDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Freedompro data API.""" + + def __init__(self, hass, api_key): + """Initialize.""" + self._hass = hass + self._api_key = api_key + self._devices = None + + update_interval = timedelta(minutes=1) + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + if self._devices is None: + result = await get_list( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + if result["state"]: + self._devices = result["devices"] + else: + raise UpdateFailed() + + result = await get_states( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + + for device in self._devices: + dev = next( + (dev for dev in result if dev["uid"] == device["uid"]), + None, + ) + if dev is not None and "state" in dev: + device["state"] = dev["state"] + return self._devices diff --git a/homeassistant/components/freedompro/config_flow.py b/homeassistant/components/freedompro/config_flow.py new file mode 100644 index 00000000000..c1288e61406 --- /dev/null +++ b/homeassistant/components/freedompro/config_flow.py @@ -0,0 +1,73 @@ +"""Config flow to configure Freedompro.""" +from pyfreedompro import get_list +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) + + +class Hub: + """Freedompro Hub class.""" + + def __init__(self, hass, api_key): + """Freedompro Hub class init.""" + self._hass = hass + self._api_key = api_key + + async def authenticate(self): + """Freedompro Hub class authenticate.""" + return await get_list( + aiohttp_client.async_get_clientsession(self._hass), self._api_key + ) + + +async def validate_input(hass: core.HomeAssistant, api_key): + """Validate api key.""" + hub = Hub(hass, api_key) + result = await hub.authenticate() + if result["state"] is False: + if result["code"] == -201: + raise InvalidAuth + if result["code"] == -200: + raise CannotConnect + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Show the setup form to the user.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input[CONF_API_KEY]) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry(title="Freedompro", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/freedompro/const.py b/homeassistant/components/freedompro/const.py new file mode 100644 index 00000000000..3f5df9283d4 --- /dev/null +++ b/homeassistant/components/freedompro/const.py @@ -0,0 +1,3 @@ +"""Constants for the Freedompro integration.""" + +DOMAIN = "freedompro" diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py new file mode 100644 index 00000000000..ca96dba00f7 --- /dev/null +++ b/homeassistant/components/freedompro/light.py @@ -0,0 +1,109 @@ +"""Support for Freedompro light.""" +import json + +from pyfreedompro import put_state + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + LightEntity, +) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Freedompro light.""" + api_key = entry.data[CONF_API_KEY] + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + Device(hass, api_key, device, coordinator) + for device in coordinator.data + if device["type"] == "lightbulb" + ) + + +class Device(CoordinatorEntity, LightEntity): + """Representation of an Freedompro light.""" + + def __init__(self, hass, api_key, device, coordinator): + """Initialize the Freedompro light.""" + super().__init__(coordinator) + self._hass = hass + self._session = aiohttp_client.async_get_clientsession(self._hass) + self._api_key = api_key + self._attr_name = device["name"] + self._attr_unique_id = device["uid"] + self._type = device["type"] + self._characteristics = device["characteristics"] + self._attr_is_on = False + self._attr_brightness = 0 + color_mode = COLOR_MODE_ONOFF + if "hue" in self._characteristics: + color_mode = COLOR_MODE_HS + elif "brightness" in self._characteristics: + color_mode = COLOR_MODE_BRIGHTNESS + self._attr_color_mode = color_mode + self._attr_supported_color_modes = {color_mode} + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + device = next( + ( + device + for device in self.coordinator.data + if device["uid"] == self._attr_unique_id + ), + None, + ) + if device is not None and "state" in device: + state = device["state"] + if "on" in state: + self._attr_is_on = state["on"] + if "brightness" in state: + self._attr_brightness = round(state["brightness"] / 100 * 255) + if "hue" in state and "saturation" in state: + self._attr_hs_color = (state["hue"], state["saturation"]) + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + async def async_turn_on(self, **kwargs): + """Async function to set on to light.""" + payload = {"on": True} + if ATTR_BRIGHTNESS in kwargs: + payload["brightness"] = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100) + if ATTR_HS_COLOR in kwargs: + payload["saturation"] = round(kwargs[ATTR_HS_COLOR][1]) + payload["hue"] = round(kwargs[ATTR_HS_COLOR][0]) + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Async function to set off to light.""" + payload = {"on": False} + payload = json.dumps(payload) + await put_state( + self._session, + self._api_key, + self._attr_unique_id, + payload, + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/freedompro/manifest.json b/homeassistant/components/freedompro/manifest.json new file mode 100644 index 00000000000..94d57b37cae --- /dev/null +++ b/homeassistant/components/freedompro/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "freedompro", + "name": "Freedompro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/freedompro", + "codeowners": [ + "@stefano055415" + ], + "requirements": ["pyfreedompro==1.1.0"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/freedompro/strings.json b/homeassistant/components/freedompro/strings.json new file mode 100644 index 00000000000..947a9bd2e33 --- /dev/null +++ b/homeassistant/components/freedompro/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Please enter the API key obtained from https://home.freedompro.eu", + "title": "Freedompro API key" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/freedompro/translations/en.json b/homeassistant/components/freedompro/translations/en.json new file mode 100644 index 00000000000..c8952d56bfd --- /dev/null +++ b/homeassistant/components/freedompro/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API key obtained from https://home.freedompro.eu", + "title": "Freedompro API key" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2f12ebd7d74..aa6d9009574 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -82,6 +82,7 @@ FLOWS = [ "forked_daapd", "foscam", "freebox", + "freedompro", "fritz", "fritzbox", "fritzbox_callmonitor", diff --git a/requirements_all.txt b/requirements_all.txt index 498e5154c21..eaa1283e750 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1450,6 +1450,9 @@ pyfnip==0.2 # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 +# homeassistant.components.freedompro +pyfreedompro==1.1.0 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 101280ddf3b..fb74d7cb53a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -809,6 +809,9 @@ pyflunearyou==1.0.7 # homeassistant.components.forked_daapd pyforked-daapd==0.1.11 +# homeassistant.components.freedompro +pyfreedompro==1.1.0 + # homeassistant.components.fritzbox pyfritzhome==0.4.2 diff --git a/tests/components/freedompro/__init__.py b/tests/components/freedompro/__init__.py new file mode 100644 index 00000000000..1f87c43b43c --- /dev/null +++ b/tests/components/freedompro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Freedompro integration.""" diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py new file mode 100644 index 00000000000..c43887fa487 --- /dev/null +++ b/tests/components/freedompro/conftest.py @@ -0,0 +1,67 @@ +"""Fixtures for Freedompro integration tests.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.freedompro.const import DOMAIN + +from tests.common import MockConfigEntry +from tests.components.freedompro.const import DEVICES, DEVICES_STATE + + +@pytest.fixture +async def init_integration(hass) -> MockConfigEntry: + """Set up the Freedompro integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), patch( + "homeassistant.components.freedompro.get_states", + return_value=DEVICES_STATE, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +@pytest.fixture +async def init_integration_no_state(hass) -> MockConfigEntry: + """Set up the Freedompro integration in Home Assistant without state.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), patch( + "homeassistant.components.freedompro.get_states", + return_value=[], + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/freedompro/const.py b/tests/components/freedompro/const.py new file mode 100644 index 00000000000..8635858d000 --- /dev/null +++ b/tests/components/freedompro/const.py @@ -0,0 +1,219 @@ +"""Const Freedompro for test.""" + +DEVICES = [ + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "name": "Bathroom leak sensor", + "type": "leakSensor", + "characteristics": ["leakDetected"], + }, + { + "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "name": "lock", + "type": "lock", + "characteristics": ["lock"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS", + "name": "Bedroom fan", + "type": "fan", + "characteristics": ["on", "rotationSpeed"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "name": "Contact sensor living room", + "type": "contactSensor", + "characteristics": ["contactSensorState"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "name": "Doorway motion sensor", + "type": "motionSensor", + "characteristics": ["motionDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY", + "name": "Garden humidity sensor", + "type": "humiditySensor", + "characteristics": ["currentRelativeHumidity"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", + "name": "Irrigation switch", + "type": "switch", + "characteristics": ["on"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M", + "name": "lightbulb", + "type": "lightbulb", + "characteristics": ["on", "brightness", "saturation", "hue"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "name": "Living room occupancy sensor", + "type": "occupancySensor", + "characteristics": ["occupancyDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA", + "name": "Living room temperature sensor", + "type": "temperatureSensor", + "characteristics": ["currentTemperature"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "name": "Smoke sensor kitchen", + "type": "smokeSensor", + "characteristics": ["smokeDetected"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C", + "name": "Bedroom CO2 sensor", + "type": "carbonDioxideSensor", + "characteristics": ["carbonDioxideDetected", "carbonDioxideLevel"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC", + "name": "bedroomlight", + "type": "lightbulb", + "characteristics": ["on"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI", + "name": "Bedroom thermostat", + "type": "thermostat", + "characteristics": [ + "heatingCoolingState", + "currentTemperature", + "targetTemperature", + ], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "name": "Bedroom window covering", + "type": "windowCovering", + "characteristics": ["position"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", + "name": "Garden light sensors", + "type": "lightSensor", + "characteristics": ["currentAmbientLightLevel"], + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I", + "name": "Living room outlet", + "type": "outlet", + "characteristics": ["on"], + }, +] + +DEVICES_STATE = [ + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "type": "leakSensor", + "state": {"leakDetected": 0}, + "online": True, + }, + { + "uid": "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0", + "type": "lock", + "state": {"lock": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS", + "type": "fan", + "state": {"on": False, "rotationSpeed": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SOT3NKALCRQMHUHJUF79NUG6UQP1IIQIN1PJVRRPT0C", + "type": "contactSensor", + "state": {"contactSensorState": True}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*VTEPEDYE8DXGS8U94CJKQDLKMN6CUX1IJWSOER2HZCK", + "type": "motionSensor", + "state": {"motionDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*QN-DDFMPEPRDOQV7W7JQG3NL0NPZGTLIBYT3HFSPNEY", + "type": "humiditySensor", + "state": {"currentRelativeHumidity": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W", + "type": "switch", + "state": {"on": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M", + "type": "lightbulb", + "state": {"on": True, "brightness": 0, "saturation": 0, "hue": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SNG7Y3R1R0S_W5BCNPP1O5WUN2NCEOOT27EFSYT6JYS", + "type": "occupancySensor", + "state": {"occupancyDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*LWPVY7X1AX0DRWLYUUNZ3ZSTHMYNDDBQTPZCZQUUASA", + "type": "temperatureSensor", + "state": {"currentTemperature": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*SXFMEXI4UMDBAMXXPI6LJV47O9NY-IRCAKZI7_MW0LY", + "type": "smokeSensor", + "state": {"smokeDetected": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*R6V0FNNF7SACWZ8V9NCOX7UCYI4ODSYAOJWZ80PLJ3C", + "type": "carbonDioxideSensor", + "state": {"carbonDioxideDetected": False, "carbonDioxideLevel": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC", + "type": "lightbulb", + "state": {"on": False}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI", + "type": "thermostat", + "state": { + "heatingCoolingState": 1, + "currentTemperature": 14, + "targetTemperature": 14, + }, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3XSSVIJWK-65HILWTC4WINQK46SP4OEZRCNO25VGWAS", + "type": "windowCovering", + "state": {"position": 0}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JVRAR_6WVL1Y0PJ5GFWGPMFV7FLVD4MZKBWXC_UFWYM", + "type": "lightSensor", + "state": {"currentAmbientLightLevel": 500}, + "online": True, + }, + { + "uid": "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*0PUTVZVJJJL-ZHZZBHTIBS3-J-U7JYNPACFPJW0MD-I", + "type": "outlet", + "state": {"on": False}, + "online": True, + }, +] diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py new file mode 100644 index 00000000000..f44cbd232ad --- /dev/null +++ b/tests/components/freedompro/test_config_flow.py @@ -0,0 +1,82 @@ +"""Define tests for the Freedompro config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.freedompro.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY + +from tests.components.freedompro.const import DEVICES + +VALID_CONFIG = { + CONF_API_KEY: "ksdjfgslkjdfksjdfksjgfksjd", +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + +async def test_invalid_auth(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "homeassistant.components.freedompro.config_flow.list", + return_value={ + "state": False, + "code": -201, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_connection_error(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "homeassistant.components.freedompro.config_flow.get_list", + return_value={ + "state": False, + "code": -200, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + with patch( + "homeassistant.components.freedompro.config_flow.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Freedompro" + assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py new file mode 100644 index 00000000000..4e9d9ec197d --- /dev/null +++ b/tests/components/freedompro/test_init.py @@ -0,0 +1,55 @@ +"""Freedompro component tests.""" +import logging +from unittest.mock import patch + +from homeassistant.components.freedompro.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from tests.common import MockConfigEntry + +LOGGER = logging.getLogger(__name__) + +ENTITY_ID = f"{DOMAIN}.fake_name" + + +async def test_async_setup_entry(hass, init_integration): + """Test a successful setup entry.""" + entry = init_integration + assert entry is not None + state = hass.states + assert state is not None + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to Freedompro is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Feedompro", + unique_id="0123456", + data={ + "api_key": "gdhsksjdhcncjdkdjndjdkdmndjdjdkd", + }, + ) + + with patch( + "homeassistant.components.freedompro.get_list", + return_value={ + "state": False, + }, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass, init_integration): + """Test successful unload of entry.""" + entry = init_integration + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/freedompro/test_light.py b/tests/components/freedompro/test_light.py new file mode 100644 index 00000000000..09a945ada03 --- /dev/null +++ b/tests/components/freedompro/test_light.py @@ -0,0 +1,155 @@ +"""Tests for the Freedompro light.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er + + +async def test_light_get_state(hass, init_integration): + """Test states of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + +async def test_light_set_on(hass, init_integration): + """Test set on of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + + +async def test_light_set_off(hass, init_integration): + """Test set off of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.bedroomlight" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes.get("friendly_name") == "bedroomlight" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*3-QURR5Q6ADA8ML1TBRG59RRGM1F9LVUZLKPYKFJQHC" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + + +async def test_light_set_brightness(hass, init_integration): + """Test set brightness of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [entity_id], ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert int(state.attributes[ATTR_BRIGHTNESS]) == 0 + + +async def test_light_set_hue(hass, init_integration): + """Test set brightness of the light.""" + init_integration + registry = er.async_get(hass) + + entity_id = "light.lightbulb" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get("friendly_name") == "lightbulb" + + entry = registry.async_get(entity_id) + assert entry + assert ( + entry.unique_id + == "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*JHJZIZ9ORJNHB7DZNBNAOSEDECVTTZ48SABTCA3WA3M" + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [entity_id], + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (352.32, 100.0), + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert int(state.attributes[ATTR_BRIGHTNESS]) == 0 + assert state.attributes[ATTR_HS_COLOR] == (0, 0) From 0ab999738b6e5f42db10de52e22e8bc63e3a324c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Jun 2021 13:32:17 +0200 Subject: [PATCH 683/750] Add statistics meta data table (#52331) * Add statistics meta data table * Tweak meta data generation --- homeassistant/components/history/__init__.py | 4 +- .../components/recorder/migration.py | 5 ++ homeassistant/components/recorder/models.py | 26 ++++++++- .../components/recorder/statistics.py | 53 +++++++++++++++++-- homeassistant/components/recorder/util.py | 3 +- homeassistant/components/sensor/recorder.py | 38 +++++++++---- tests/components/history/test_init.py | 22 +++++--- 7 files changed, 126 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 79d288398c5..8fa9fe879f5 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -173,12 +173,12 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistics = await hass.async_add_executor_job( + statistic_ids = await hass.async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_type"), ) - connection.send_result(msg["id"], {"statistic_ids": statistics}) + connection.send_result(msg["id"], statistic_ids) class HistoryPeriodView(HomeAssistantView): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 02c74635f03..b219209b386 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -452,6 +452,11 @@ def _apply_update(engine, session, new_version, old_version): _drop_foreign_key_constraints( connection, engine, TABLE_STATES, ["old_state_id"] ) + elif new_version == 17: + if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + # Recreate the statistics table + Statistics.__table__.drop(engine) + Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ac3f6c9e401..5a96cbf4f0b 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -36,7 +36,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 16 +SCHEMA_VERSION = 17 _LOGGER = logging.getLogger(__name__) @@ -47,6 +47,7 @@ TABLE_STATES = "states" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" ALL_TABLES = [ TABLE_STATES, @@ -54,6 +55,7 @@ ALL_TABLES = [ TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, TABLE_STATISTICS, + TABLE_STATISTICS_META, ] DATETIME_TYPE = DateTime(timezone=True).with_variant( @@ -248,6 +250,28 @@ class Statistics(Base): # type: ignore ) +class StatisticsMeta(Base): # type: ignore + """Statistics meta data.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_STATISTICS_META + statistic_id = Column(String(255), primary_key=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + + @staticmethod + def from_meta(source, statistic_id, unit_of_measurement): + """Create object from meta data.""" + return StatisticsMeta( + source=source, + statistic_id=statistic_id, + unit_of_measurement=unit_of_measurement, + ) + + class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index cd2b72ad43a..4268ac054fe 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -13,7 +13,7 @@ from sqlalchemy.ext import baked import homeassistant.util.dt as dt_util from .const import DOMAIN -from .models import Statistics, process_timestamp_to_utc_isoformat +from .models import Statistics, StatisticsMeta, process_timestamp_to_utc_isoformat from .util import execute, retryable_database_job, session_scope if TYPE_CHECKING: @@ -34,7 +34,13 @@ QUERY_STATISTIC_IDS = [ Statistics.statistic_id, ] +QUERY_STATISTIC_META = [ + StatisticsMeta.statistic_id, + StatisticsMeta.unit_of_measurement, +] + STATISTICS_BAKERY = "recorder_statistics_bakery" +STATISTICS_META_BAKERY = "recorder_statistics_bakery" _LOGGER = logging.getLogger(__name__) @@ -42,6 +48,7 @@ _LOGGER = logging.getLogger(__name__) def async_setup(hass): """Set up the history hooks.""" hass.data[STATISTICS_BAKERY] = baked.bakery() + hass.data[STATISTICS_META_BAKERY] = baked.bakery() def get_start_time() -> datetime.datetime: @@ -73,11 +80,47 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: with session_scope(session=instance.get_session()) as session: # type: ignore for stats in platform_stats: for entity_id, stat in stats.items(): - session.add(Statistics.from_stats(DOMAIN, entity_id, start, stat)) + session.add( + Statistics.from_stats(DOMAIN, entity_id, start, stat["stat"]) + ) + exists = session.query( + session.query(StatisticsMeta) + .filter_by(statistic_id=entity_id) + .exists() + ).scalar() + if not exists: + unit = stat["meta"]["unit_of_measurement"] + session.add(StatisticsMeta.from_meta(DOMAIN, entity_id, unit)) return True +def _get_meta_data(hass, session, statistic_ids): + """Fetch meta data.""" + + def _meta(metas, wanted_statistic_id): + meta = {"statistic_id": wanted_statistic_id, "unit_of_measurement": None} + for statistic_id, unit in metas: + if statistic_id == wanted_statistic_id: + meta["unit_of_measurement"] = unit + return meta + + baked_query = hass.data[STATISTICS_META_BAKERY]( + lambda session: session.query(*QUERY_STATISTIC_META) + ) + if statistic_ids is not None: + baked_query += lambda q: q.filter( + StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) + ) + + result = execute(baked_query(session).params(statistic_ids=statistic_ids)) + + if statistic_ids is None: + statistic_ids = [statistic_id[0] for statistic_id in result] + + return {id: _meta(result, id) for id in statistic_ids} + + def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids.""" with session_scope(hass=hass) as session: @@ -92,10 +135,10 @@ def list_statistic_ids(hass, statistic_type=None): baked_query += lambda q: q.order_by(Statistics.statistic_id) - statistic_ids = [] result = execute(baked_query(session)) - statistic_ids = [statistic_id[0] for statistic_id in result] - return statistic_ids + statistic_ids_list = [statistic_id[0] for statistic_id in result] + + return list(_get_meta_data(hass, session, statistic_ids_list).values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index db9fb46425b..80c90ccaa20 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -22,6 +22,7 @@ from .models import ( TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, TABLE_STATISTICS, + TABLE_STATISTICS_META, RecorderRuns, process_timestamp, ) @@ -179,7 +180,7 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - if table == TABLE_STATISTICS: + if table in [TABLE_STATISTICS, TABLE_STATISTICS_META]: continue if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): cursor.execute(f"SELECT * FROM {table};") # nosec # not injection diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 68962081035..95f9a6faebf 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -30,6 +30,7 @@ from homeassistant.const import ( PRESSURE_MBAR, PRESSURE_PA, PRESSURE_PSI, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util @@ -49,6 +50,13 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, } +DEVICE_CLASS_UNITS = { + DEVICE_CLASS_ENERGY: ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER: POWER_WATT, + DEVICE_CLASS_PRESSURE: PRESSURE_PA, + DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS, +} + UNIT_CONVERSIONS = { DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, @@ -134,12 +142,16 @@ def _time_weighted_average( def _normalize_states( entity_history: list[State], device_class: str, entity_id: str -) -> list[tuple[float, State]]: +) -> tuple[str | None, list[tuple[float, State]]]: """Normalize units.""" if device_class not in UNIT_CONVERSIONS: # We're not normalizing this device class, return the state as they are - return [(float(el.state), el) for el in entity_history if _is_number(el.state)] + fstates = [ + (float(el.state), el) for el in entity_history if _is_number(el.state) + ] + unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + return unit, fstates fstates = [] @@ -159,7 +171,7 @@ def _normalize_states( fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) # type: ignore - return fstates + return DEVICE_CLASS_UNITS[device_class], fstates def compile_statistics( @@ -185,21 +197,25 @@ def compile_statistics( continue entity_history = history_list[entity_id] - fstates = _normalize_states(entity_history, device_class, entity_id) + unit, fstates = _normalize_states(entity_history, device_class, entity_id) if not fstates: continue result[entity_id] = {} + # Set meta data + result[entity_id]["meta"] = {"unit_of_measurement": unit} + # Make calculations + stat: dict = {} if "max" in wanted_statistics: - result[entity_id]["max"] = max(*itertools.islice(zip(*fstates), 1)) + stat["max"] = max(*itertools.islice(zip(*fstates), 1)) if "min" in wanted_statistics: - result[entity_id]["min"] = min(*itertools.islice(zip(*fstates), 1)) + stat["min"] = min(*itertools.islice(zip(*fstates), 1)) if "mean" in wanted_statistics: - result[entity_id]["mean"] = _time_weighted_average(fstates, start, end) + stat["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: last_reset = old_last_reset = None @@ -233,8 +249,10 @@ def compile_statistics( # Update the sum with the last state _sum += new_state - old_state - result[entity_id]["last_reset"] = dt_util.parse_datetime(last_reset) - result[entity_id]["sum"] = _sum - result[entity_id]["state"] = new_state + stat["last_reset"] = dt_util.parse_datetime(last_reset) + stat["sum"] = _sum + stat["state"] = new_state + + result[entity_id]["stat"] = stat return result diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 32205512474..a6c386ea319 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -951,7 +951,11 @@ async def test_list_statistic_ids(hass, hass_ws_client): hass.states.async_set( "sensor.test", 10, - attributes={"device_class": "temperature", "state_class": "measurement"}, + attributes={ + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", + }, ) await hass.async_block_till_done() @@ -962,7 +966,7 @@ async def test_list_statistic_ids(hass, hass_ws_client): await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistic_ids": []} + assert response["result"] == [] hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) @@ -970,25 +974,31 @@ async def test_list_statistic_ids(hass, hass_ws_client): await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistic_ids": ["sensor.test"]} + assert response["result"] == [ + {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + ] await client.send_json( {"id": 3, "type": "history/list_statistic_ids", "statistic_type": "dogs"} ) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistic_ids": ["sensor.test"]} + assert response["result"] == [ + {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + ] await client.send_json( {"id": 4, "type": "history/list_statistic_ids", "statistic_type": "mean"} ) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistic_ids": ["sensor.test"]} + assert response["result"] == [ + {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + ] await client.send_json( {"id": 5, "type": "history/list_statistic_ids", "statistic_type": "sum"} ) response = await client.receive_json() assert response["success"] - assert response["result"] == {"statistic_ids": []} + assert response["result"] == [] From 508f9a82969fd9a1d137ae0f16e37619f2b50cd4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Jun 2021 13:34:28 +0200 Subject: [PATCH 684/750] Update frontend to 20210630.0 (#52336) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 8f5f5f091d9..42ade2d4ae9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210603.0" + "home-assistant-frontend==20210630.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 000711f40e1..2e166af6172 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210603.0 +home-assistant-frontend==20210630.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index eaa1283e750..d0fa388e602 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210603.0 +home-assistant-frontend==20210630.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb74d7cb53a..873bfc34ef7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210603.0 +home-assistant-frontend==20210630.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 0476c7f9eef8aacbfbf6e332290ce2699ac27a57 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Jun 2021 14:17:58 +0200 Subject: [PATCH 685/750] =?UTF-8?q?Normalize=20temperature=20statistics=20?= =?UTF-8?q?to=20=C2=B0C=20(#52297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Normalize temperature statistics to °C * Fix tests * Support temperature conversion to and from K, improve tests * Fix test * Add tests, pylint --- homeassistant/components/sensor/recorder.py | 14 +++- homeassistant/util/temperature.py | 38 ++++++++- tests/components/history/test_init.py | 6 +- tests/components/recorder/test_statistics.py | 7 +- tests/components/sensor/test_recorder.py | 14 +++- tests/util/test_temperature.py | 84 ++++++++++++++++++++ tests/util/test_unit_system.py | 2 +- 7 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 tests/util/test_temperature.py diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 95f9a6faebf..ac26e06e07d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -4,6 +4,7 @@ from __future__ import annotations import datetime import itertools import logging +from typing import Callable from homeassistant.components.recorder import history, statistics from homeassistant.components.sensor import ( @@ -31,10 +32,13 @@ from homeassistant.const import ( PRESSURE_PA, PRESSURE_PSI, TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, ) from homeassistant.core import HomeAssistant, State import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util +import homeassistant.util.temperature as temperature_util from . import DOMAIN @@ -57,7 +61,7 @@ DEVICE_CLASS_UNITS = { DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS, } -UNIT_CONVERSIONS = { +UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, ENERGY_WATT_HOUR: lambda x: x / 1000, @@ -74,6 +78,11 @@ UNIT_CONVERSIONS = { PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], }, + DEVICE_CLASS_TEMPERATURE: { + TEMP_CELSIUS: lambda x: x, + TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, + TEMP_KELVIN: temperature_util.kelvin_to_celsius, + }, } WARN_UNSUPPORTED_UNIT = set() @@ -169,7 +178,7 @@ def _normalize_states( _LOGGER.warning("%s has unknown unit %s", entity_id, unit) continue - fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) # type: ignore + fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) return DEVICE_CLASS_UNITS[device_class], fstates @@ -229,6 +238,7 @@ def compile_statistics( _sum = last_stats[entity_id][0]["sum"] for fstate, state in fstates: + if "last_reset" not in state.attributes: continue if (last_reset := state.attributes["last_reset"]) != old_last_reset: diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 0b3edc6ef57..bc3cb4c1017 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -2,6 +2,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, + TEMP_KELVIN, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE, ) @@ -14,6 +15,13 @@ def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: return (fahrenheit - 32.0) / 1.8 +def kelvin_to_celsius(kelvin: float, interval: bool = False) -> float: + """Convert a temperature in Kelvin to Celsius.""" + if interval: + return kelvin + return kelvin - 273.15 + + def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" if interval: @@ -21,17 +29,39 @@ def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float: return celsius * 1.8 + 32.0 +def celsius_to_kelvin(celsius: float, interval: bool = False) -> float: + """Convert a temperature in Celsius to Fahrenheit.""" + if interval: + return celsius + return celsius + 273.15 + + def convert( temperature: float, from_unit: str, to_unit: str, interval: bool = False ) -> float: """Convert a temperature from one unit to another.""" - if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE)) - if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): + if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN): raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE)) if from_unit == to_unit: return temperature + if from_unit == TEMP_CELSIUS: - return celsius_to_fahrenheit(temperature, interval) - return fahrenheit_to_celsius(temperature, interval) + if to_unit == TEMP_FAHRENHEIT: + return celsius_to_fahrenheit(temperature, interval) + # kelvin + return celsius_to_kelvin(temperature, interval) + + if from_unit == TEMP_FAHRENHEIT: + if to_unit == TEMP_CELSIUS: + return fahrenheit_to_celsius(temperature, interval) + # kelvin + return celsius_to_kelvin(fahrenheit_to_celsius(temperature, interval), interval) + + # from_unit == kelvin + if to_unit == TEMP_CELSIUS: + return kelvin_to_celsius(temperature, interval) + # fahrenheit + return celsius_to_fahrenheit(kelvin_to_celsius(temperature, interval), interval) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index a6c386ea319..ac57069acf0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -840,7 +840,11 @@ async def test_statistics_during_period(hass, hass_ws_client): hass.states.async_set( "sensor.test", 10, - attributes={"device_class": "temperature", "state_class": "measurement"}, + attributes={ + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", + }, ) await hass.async_block_till_done() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 679ef7597c2..104617aee2c 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -9,6 +9,7 @@ from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -53,7 +54,11 @@ def record_states(hass): sns1 = "sensor.test1" sns2 = "sensor.test2" sns3 = "sensor.test3" - sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns1_attr = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": TEMP_CELSIUS, + } sns2_attr = {"device_class": "temperature"} sns3_attr = {} diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b8411e69a7f..65346f1feba 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -9,7 +9,7 @@ from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import statistics_during_period -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -324,7 +324,11 @@ def record_states(hass): sns1 = "sensor.test1" sns2 = "sensor.test2" sns3 = "sensor.test3" - sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns1_attr = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": TEMP_CELSIUS, + } sns2_attr = {"device_class": "temperature"} sns3_attr = {} @@ -466,7 +470,11 @@ def record_states_partially_unavailable(hass): sns1 = "sensor.test1" sns2 = "sensor.test2" sns3 = "sensor.test3" - sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns1_attr = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": TEMP_CELSIUS, + } sns2_attr = {"device_class": "temperature"} sns3_attr = {} diff --git a/tests/util/test_temperature.py b/tests/util/test_temperature.py new file mode 100644 index 00000000000..7730a89cbb8 --- /dev/null +++ b/tests/util/test_temperature.py @@ -0,0 +1,84 @@ +"""Test Home Assistant temperature utility functions.""" +import pytest + +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN +import homeassistant.util.temperature as temperature_util + +INVALID_SYMBOL = "bob" +VALID_SYMBOL = TEMP_CELSIUS + + +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2 + assert temperature_util.convert(3, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT) == 3 + assert temperature_util.convert(4, TEMP_KELVIN, TEMP_KELVIN) == 4 + + +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + temperature_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with pytest.raises(ValueError): + temperature_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + temperature_util.convert("a", TEMP_CELSIUS, TEMP_FAHRENHEIT) + + +def test_convert_from_celsius(): + """Test conversion from C to other units.""" + celsius = 100 + assert temperature_util.convert( + celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT + ) == pytest.approx(212.0) + assert temperature_util.convert( + celsius, TEMP_CELSIUS, TEMP_KELVIN + ) == pytest.approx(373.15) + # Interval + assert temperature_util.convert( + celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT, True + ) == pytest.approx(180.0) + assert temperature_util.convert( + celsius, TEMP_CELSIUS, TEMP_KELVIN, True + ) == pytest.approx(100) + + +def test_convert_from_fahrenheit(): + """Test conversion from F to other units.""" + fahrenheit = 100 + assert temperature_util.convert( + fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) == pytest.approx(37.77777777777778) + assert temperature_util.convert( + fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN + ) == pytest.approx(310.92777777777775) + # Interval + assert temperature_util.convert( + fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS, True + ) == pytest.approx(55.55555555555556) + assert temperature_util.convert( + fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN, True + ) == pytest.approx(55.55555555555556) + + +def test_convert_from_kelvin(): + """Test conversion from K to other units.""" + kelvin = 100 + assert temperature_util.convert(kelvin, TEMP_KELVIN, TEMP_CELSIUS) == pytest.approx( + -173.15 + ) + assert temperature_util.convert( + kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT + ) == pytest.approx(-279.66999999999996) + # Interval + assert temperature_util.convert( + kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT, True + ) == pytest.approx(180.0) + assert temperature_util.convert( + kelvin, TEMP_KELVIN, TEMP_KELVIN, True + ) == pytest.approx(100) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 74abfef452f..f32e731f9b3 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -106,7 +106,7 @@ def test_temperature_same_unit(): def test_temperature_unknown_unit(): """Test no conversion happens if unknown unit.""" with pytest.raises(ValueError): - METRIC_SYSTEM.temperature(5, "K") + METRIC_SYSTEM.temperature(5, "abc") def test_temperature_to_metric(): From c0751c060fd720a44a9ce3ca7285d2a6ceb90581 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 30 Jun 2021 14:34:33 +0200 Subject: [PATCH 686/750] review comments. (#52337) --- tests/components/modbus/test_binary_sensor.py | 28 +++---- tests/components/modbus/test_climate.py | 26 +++---- tests/components/modbus/test_cover.py | 60 +++++++------- tests/components/modbus/test_fan.py | 70 ++++++++--------- tests/components/modbus/test_light.py | 70 ++++++++--------- tests/components/modbus/test_sensor.py | 54 ++++++------- tests/components/modbus/test_switch.py | 78 +++++++++---------- 7 files changed, 193 insertions(+), 193 deletions(-) diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 0f388edb3e8..e9c178ff025 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -21,8 +21,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_test, prepare_service_update -sensor_name = "test_binary_sensor" -entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" +SENSOR_NAME = "test_binary_sensor" +ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" @pytest.mark.parametrize( @@ -31,7 +31,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, } ] @@ -39,7 +39,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_BINARY_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, @@ -88,8 +88,8 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): """Run test for given config.""" state = await base_test( hass, - {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, - sensor_name, + {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: do_type}, + SENSOR_NAME, SENSOR_DOMAIN, CONF_BINARY_SENSORS, None, @@ -107,7 +107,7 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): config = { CONF_BINARY_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -119,21 +119,21 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF mock_pymodbus.read_coils.return_value = ReadResult([0x01]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON @pytest.mark.parametrize( "mock_test_state", - [(State(entity_id, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),)], indirect=True, ) @pytest.mark.parametrize( @@ -142,7 +142,7 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): { CONF_BINARY_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, } ] @@ -151,4 +151,4 @@ async def test_service_binary_sensor_update(hass, mock_pymodbus): ) async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus): """Run test for binary sensor restore state.""" - assert hass.states.get(entity_id).state == mock_test_state[0].state + assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index adbf5897be3..b58822644be 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -16,8 +16,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_test, prepare_service_update -climate_name = "test_climate" -entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" +CLIMATE_NAME = "test_climate" +ENTITY_ID = f"{CLIMATE_DOMAIN}.{CLIMATE_NAME}" @pytest.mark.parametrize( @@ -26,7 +26,7 @@ entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" { CONF_CLIMATES: [ { - CONF_NAME: climate_name, + CONF_NAME: CLIMATE_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -36,7 +36,7 @@ entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" { CONF_CLIMATES: [ { - CONF_NAME: climate_name, + CONF_NAME: CLIMATE_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -63,18 +63,18 @@ async def test_config_climate(hass, mock_modbus): ) async def test_temperature_climate(hass, regs, expected): """Run test for given config.""" - climate_name = "modbus_test_climate" + CLIMATE_NAME = "modbus_test_climate" return state = await base_test( hass, { - CONF_NAME: climate_name, + CONF_NAME: CLIMATE_NAME, CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_COUNT: 2, }, - climate_name, + CLIMATE_NAME, CLIMATE_DOMAIN, CONF_CLIMATES, None, @@ -92,7 +92,7 @@ async def test_service_climate_update(hass, mock_pymodbus): config = { CONF_CLIMATES: [ { - CONF_NAME: climate_name, + CONF_NAME: CLIMATE_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, @@ -105,12 +105,12 @@ async def test_service_climate_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == "auto" + assert hass.states.get(ENTITY_ID).state == "auto" -test_value = State(entity_id, 35) +test_value = State(ENTITY_ID, 35) test_value.attributes = {ATTR_TEMPERATURE: 37} @@ -125,7 +125,7 @@ test_value.attributes = {ATTR_TEMPERATURE: 37} { CONF_CLIMATES: [ { - CONF_NAME: climate_name, + CONF_NAME: CLIMATE_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, } @@ -135,6 +135,6 @@ test_value.attributes = {ATTR_TEMPERATURE: 37} ) async def test_restore_state_climate(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - state = hass.states.get(entity_id) + state = hass.states.get(ENTITY_ID) assert state.state == HVAC_MODE_AUTO assert state.attributes[ATTR_TEMPERATURE] == 37 diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index c7b5d827788..37274603bee 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -31,8 +31,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_test, prepare_service_update -cover_name = "test_cover" -entity_id = f"{COVER_DOMAIN}.{cover_name}" +COVER_NAME = "test_cover" +ENTITY_ID = f"{COVER_DOMAIN}.{COVER_NAME}" @pytest.mark.parametrize( @@ -41,7 +41,7 @@ entity_id = f"{COVER_DOMAIN}.{cover_name}" { CONF_COVERS: [ { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_COIL, } @@ -50,7 +50,7 @@ entity_id = f"{COVER_DOMAIN}.{cover_name}" { CONF_COVERS: [ { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SLAVE: 10, @@ -95,12 +95,12 @@ async def test_coil_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - cover_name, + COVER_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -142,11 +142,11 @@ async def test_register_cover(hass, regs, expected): state = await base_test( hass, { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, }, - cover_name, + COVER_NAME, COVER_DOMAIN, CONF_COVERS, None, @@ -164,7 +164,7 @@ async def test_service_cover_update(hass, mock_pymodbus): config = { CONF_COVERS: [ { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, } @@ -176,23 +176,23 @@ async def test_service_cover_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_CLOSED + assert hass.states.get(ENTITY_ID).state == STATE_CLOSED mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_OPEN + assert hass.states.get(ENTITY_ID).state == STATE_OPEN @pytest.mark.parametrize( "mock_test_state", [ - (State(entity_id, STATE_CLOSED),), - (State(entity_id, STATE_CLOSING),), - (State(entity_id, STATE_OPENING),), - (State(entity_id, STATE_OPEN),), + (State(ENTITY_ID, STATE_CLOSED),), + (State(ENTITY_ID, STATE_CLOSING),), + (State(ENTITY_ID, STATE_OPENING),), + (State(ENTITY_ID, STATE_OPEN),), ], indirect=True, ) @@ -202,7 +202,7 @@ async def test_service_cover_update(hass, mock_pymodbus): { CONF_COVERS: [ { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, CONF_STATE_OPEN: 1, @@ -219,22 +219,22 @@ async def test_service_cover_update(hass, mock_pymodbus): async def test_restore_state_cover(hass, mock_test_state, mock_modbus): """Run test for cover restore state.""" test_state = mock_test_state[0].state - assert hass.states.get(entity_id).state == test_state + assert hass.states.get(ENTITY_ID).state == test_state async def test_service_cover_move(hass, mock_pymodbus): """Run test for service homeassistant.update_entity.""" - entity_id2 = f"{entity_id}2" + ENTITY_ID2 = f"{ENTITY_ID}2" config = { CONF_COVERS: [ { - CONF_NAME: cover_name, + CONF_NAME: COVER_NAME, CONF_ADDRESS: 1234, CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: f"{cover_name}2", + CONF_NAME: f"{COVER_NAME}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1234, }, @@ -246,26 +246,26 @@ async def test_service_cover_move(hass, mock_pymodbus): config, ) await hass.services.async_call( - "cover", "open_cover", {"entity_id": entity_id}, blocking=True + "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_OPEN + assert hass.states.get(ENTITY_ID).state == STATE_OPEN mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( - "cover", "close_cover", {"entity_id": entity_id}, blocking=True + "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_CLOSED + assert hass.states.get(ENTITY_ID).state == STATE_CLOSED mock_pymodbus.reset() mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( - "cover", "close_cover", {"entity_id": entity_id}, blocking=True + "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True ) assert mock_pymodbus.read_holding_registers.called - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") await hass.services.async_call( - "cover", "close_cover", {"entity_id": entity_id2}, blocking=True + "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) - assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index d5420000e3e..4eeb094130b 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -34,8 +34,8 @@ from homeassistant.setup import async_setup_component from .conftest import ReadResult, base_test, prepare_service_update -fan_name = "test_fan" -entity_id = f"{FAN_DOMAIN}.{fan_name}" +FAN_NAME = "test_fan" +ENTITY_ID = f"{FAN_DOMAIN}.{FAN_NAME}" @pytest.mark.parametrize( @@ -44,7 +44,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, } ] @@ -52,7 +52,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -61,7 +61,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -78,7 +78,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -95,7 +95,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -112,7 +112,7 @@ entity_id = f"{FAN_DOMAIN}.{fan_name}" { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -164,13 +164,13 @@ async def test_all_fan(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - fan_name, + FAN_NAME, FAN_DOMAIN, CONF_FANS, None, @@ -184,7 +184,7 @@ async def test_all_fan(hass, call_type, regs, verify, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(entity_id, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),)], indirect=True, ) @pytest.mark.parametrize( @@ -193,7 +193,7 @@ async def test_all_fan(hass, call_type, regs, verify, expected): { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, } ] @@ -202,13 +202,13 @@ async def test_all_fan(hass, call_type, regs, verify, expected): ) async def test_restore_state_fan(hass, mock_test_state, mock_modbus): """Run test for fan restore state.""" - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON async def test_fan_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id2 = f"{FAN_DOMAIN}.{fan_name}2" + ENTITY_ID2 = f"{FAN_DOMAIN}.{FAN_NAME}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -216,12 +216,12 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: f"{fan_name}2", + CONF_NAME: f"{FAN_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -233,44 +233,44 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF await hass.services.async_call( - "fan", "turn_on", service_data={"entity_id": entity_id} + "fan", "turn_on", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON await hass.services.async_call( - "fan", "turn_off", service_data={"entity_id": entity_id} + "fan", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( - "fan", "turn_on", service_data={"entity_id": entity_id2} + "fan", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_ON + assert hass.states.get(ENTITY_ID2).state == STATE_ON mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( - "fan", "turn_off", service_data={"entity_id": entity_id2} + "fan", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF mock_pymodbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( - "fan", "turn_on", service_data={"entity_id": entity_id2} + "fan", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "fan", "turn_off", service_data={"entity_id": entity_id} + "fan", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE async def test_service_fan_update(hass, mock_pymodbus): @@ -279,7 +279,7 @@ async def test_service_fan_update(hass, mock_pymodbus): config = { CONF_FANS: [ { - CONF_NAME: fan_name, + CONF_NAME: FAN_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, @@ -292,11 +292,11 @@ async def test_service_fan_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 51d418a771f..e962b69a2a6 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -34,8 +34,8 @@ from homeassistant.setup import async_setup_component from .conftest import ReadResult, base_test, prepare_service_update -light_name = "test_light" -entity_id = f"{LIGHT_DOMAIN}.{light_name}" +LIGHT_NAME = "test_light" +ENTITY_ID = f"{LIGHT_DOMAIN}.{LIGHT_NAME}" @pytest.mark.parametrize( @@ -44,7 +44,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, } ] @@ -52,7 +52,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -61,7 +61,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -78,7 +78,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -95,7 +95,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -112,7 +112,7 @@ entity_id = f"{LIGHT_DOMAIN}.{light_name}" { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -164,13 +164,13 @@ async def test_all_light(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - light_name, + LIGHT_NAME, LIGHT_DOMAIN, CONF_LIGHTS, None, @@ -184,7 +184,7 @@ async def test_all_light(hass, call_type, regs, verify, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(entity_id, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),)], indirect=True, ) @pytest.mark.parametrize( @@ -193,7 +193,7 @@ async def test_all_light(hass, call_type, regs, verify, expected): { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, } ] @@ -202,13 +202,13 @@ async def test_all_light(hass, call_type, regs, verify, expected): ) async def test_restore_state_light(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - assert hass.states.get(entity_id).state == mock_test_state[0].state + assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state async def test_light_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id2 = f"{entity_id}2" + ENTITY_ID2 = f"{ENTITY_ID}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -216,12 +216,12 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: f"{light_name}2", + CONF_NAME: f"{LIGHT_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -233,44 +233,44 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity_id} + "light", "turn_on", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity_id} + "light", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity_id2} + "light", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_ON + assert hass.states.get(ENTITY_ID2).state == STATE_ON mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity_id2} + "light", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF mock_pymodbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity_id2} + "light", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity_id} + "light", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE async def test_service_light_update(hass, mock_pymodbus): @@ -279,7 +279,7 @@ async def test_service_light_update(hass, mock_pymodbus): config = { CONF_LIGHTS: [ { - CONF_NAME: light_name, + CONF_NAME: LIGHT_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, @@ -292,11 +292,11 @@ async def test_service_light_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index a5e83780660..f9bc8454281 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -38,8 +38,8 @@ from homeassistant.core import State from .conftest import ReadResult, base_config_test, base_test, prepare_service_update -sensor_name = "test_sensor" -entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" +SENSOR_NAME = "test_sensor" +ENTITY_ID = f"{SENSOR_DOMAIN}.{SENSOR_NAME}" @pytest.mark.parametrize( @@ -48,7 +48,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, } ] @@ -56,7 +56,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -72,7 +72,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, CONF_COUNT: 1, @@ -88,7 +88,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_NONE, @@ -98,7 +98,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_COUNT: 1, CONF_SWAP: CONF_SWAP_BYTE, @@ -108,7 +108,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD, @@ -118,7 +118,7 @@ entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, CONF_COUNT: 2, CONF_SWAP: CONF_SWAP_WORD_BYTE, @@ -212,7 +212,7 @@ async def test_config_wrong_struct_sensor( """Run test for sensor with wrong struct.""" config_sensor = { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, **do_config, } caplog.set_level(logging.WARNING) @@ -221,7 +221,7 @@ async def test_config_wrong_struct_sensor( await base_config_test( hass, config_sensor, - sensor_name, + SENSOR_NAME, SENSOR_DOMAIN, CONF_SENSORS, None, @@ -507,8 +507,8 @@ async def test_all_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg}, - sensor_name, + {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, + SENSOR_NAME, SENSOR_DOMAIN, CONF_SENSORS, CONF_REGISTERS, @@ -561,8 +561,8 @@ async def test_struct_sensor(hass, cfg, regs, expected): state = await base_test( hass, - {CONF_NAME: sensor_name, CONF_ADDRESS: 1234, **cfg}, - sensor_name, + {CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, **cfg}, + SENSOR_NAME, SENSOR_DOMAIN, CONF_SENSORS, None, @@ -576,7 +576,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(entity_id, "117"),)], + [(State(ENTITY_ID, "117"),)], indirect=True, ) @pytest.mark.parametrize( @@ -585,7 +585,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 51, } ] @@ -594,7 +594,7 @@ async def test_struct_sensor(hass, cfg, regs, expected): ) async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - assert hass.states.get(entity_id).state == mock_test_state[0].state + assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state @pytest.mark.parametrize( @@ -602,11 +602,11 @@ async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): [ ( CONF_SWAP_WORD, - f"Error in sensor {sensor_name} swap(word) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {SENSOR_NAME} swap(word) not possible due to the registers count: 1, needed: 2", ), ( CONF_SWAP_WORD_BYTE, - f"Error in sensor {sensor_name} swap(word_byte) not possible due to the registers count: 1, needed: 2", + f"Error in sensor {SENSOR_NAME} swap(word_byte) not possible due to the registers count: 1, needed: 2", ), ], ) @@ -615,7 +615,7 @@ async def test_swap_sensor_wrong_config( ): """Run test for sensor swap.""" config = { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_COUNT: 1, CONF_SWAP: swap_type, @@ -627,7 +627,7 @@ async def test_swap_sensor_wrong_config( await base_config_test( hass, config, - sensor_name, + SENSOR_NAME, SENSOR_DOMAIN, CONF_SENSORS, None, @@ -642,7 +642,7 @@ async def test_service_sensor_update(hass, mock_pymodbus): config = { CONF_SENSORS: [ { - CONF_NAME: sensor_name, + CONF_NAME: SENSOR_NAME, CONF_ADDRESS: 1234, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, } @@ -654,11 +654,11 @@ async def test_service_sensor_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == "27" + assert hass.states.get(ENTITY_ID).state == "27" mock_pymodbus.read_input_registers.return_value = ReadResult([32]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == "32" + assert hass.states.get(ENTITY_ID).state == "32" diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index f305f192f38..b31ca12c48b 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -43,8 +43,8 @@ from .conftest import ReadResult, base_test, prepare_service_update from tests.common import async_fire_time_changed -switch_name = "test_switch" -entity_id = f"{SWITCH_DOMAIN}.{switch_name}" +SWITCH_NAME = "test_switch" +ENTITY_ID = f"{SWITCH_DOMAIN}.{SWITCH_NAME}" @pytest.mark.parametrize( @@ -53,7 +53,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, } ] @@ -61,7 +61,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, } @@ -70,7 +70,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -88,7 +88,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -107,7 +107,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -125,7 +125,7 @@ entity_id = f"{SWITCH_DOMAIN}.{switch_name}" { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_COMMAND_OFF: 0x00, @@ -179,13 +179,13 @@ async def test_all_switch(hass, call_type, regs, verify, expected): state = await base_test( hass, { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_SLAVE: 1, CONF_WRITE_TYPE: call_type, **verify, }, - switch_name, + SWITCH_NAME, SWITCH_DOMAIN, CONF_SWITCHES, None, @@ -199,7 +199,7 @@ async def test_all_switch(hass, call_type, regs, verify, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(entity_id, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),)], indirect=True, ) @pytest.mark.parametrize( @@ -208,7 +208,7 @@ async def test_all_switch(hass, call_type, regs, verify, expected): { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, } ] @@ -217,13 +217,13 @@ async def test_all_switch(hass, call_type, regs, verify, expected): ) async def test_restore_state_switch(hass, mock_test_state, mock_modbus): """Run test for sensor restore state.""" - assert hass.states.get(entity_id).state == mock_test_state[0].state + assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state async def test_switch_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - entity_id2 = f"{SWITCH_DOMAIN}.{switch_name}2" + ENTITY_ID2 = f"{SWITCH_DOMAIN}.{SWITCH_NAME}2" config = { MODBUS_DOMAIN: { CONF_TYPE: "tcp", @@ -231,12 +231,12 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): CONF_PORT: 5501, CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, }, { - CONF_NAME: f"{switch_name}2", + CONF_NAME: f"{SWITCH_NAME}2", CONF_ADDRESS: 17, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_VERIFY: {}, @@ -248,44 +248,44 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF await hass.services.async_call( - "switch", "turn_on", service_data={"entity_id": entity_id} + "switch", "turn_on", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON await hass.services.async_call( - "switch", "turn_off", service_data={"entity_id": entity_id} + "switch", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( - "switch", "turn_on", service_data={"entity_id": entity_id2} + "switch", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_ON + assert hass.states.get(ENTITY_ID2).state == STATE_ON mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( - "switch", "turn_off", service_data={"entity_id": entity_id2} + "switch", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_OFF + assert hass.states.get(ENTITY_ID2).state == STATE_OFF mock_pymodbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( - "switch", "turn_on", service_data={"entity_id": entity_id2} + "switch", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() - assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( - "switch", "turn_off", service_data={"entity_id": entity_id} + "switch", "turn_off", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE async def test_service_switch_update(hass, mock_pymodbus): @@ -294,7 +294,7 @@ async def test_service_switch_update(hass, mock_pymodbus): config = { CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 1234, CONF_WRITE_TYPE: CALL_TYPE_COIL, CONF_VERIFY: {}, @@ -307,14 +307,14 @@ async def test_service_switch_update(hass, mock_pymodbus): config, ) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON mock_pymodbus.read_coils.return_value = ReadResult([0x00]) await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF async def test_delay_switch(hass, mock_pymodbus): @@ -327,7 +327,7 @@ async def test_delay_switch(hass, mock_pymodbus): CONF_PORT: 5501, CONF_SWITCHES: [ { - CONF_NAME: switch_name, + CONF_NAME: SWITCH_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: { @@ -345,12 +345,12 @@ async def test_delay_switch(hass, mock_pymodbus): assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True await hass.async_block_till_done() await hass.services.async_call( - "switch", "turn_on", service_data={"entity_id": entity_id} + "switch", "turn_on", service_data={"entity_id": ENTITY_ID} ) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(ENTITY_ID).state == STATE_OFF now = now + timedelta(seconds=2) with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): async_fire_time_changed(hass, now) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(ENTITY_ID).state == STATE_ON From 9d0c4c168e39e99d68bd34f01c7e8108495fe52d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Jun 2021 14:53:42 +0200 Subject: [PATCH 687/750] Convert units when fetching statistics (#52338) --- .../components/recorder/statistics.py | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4268ac054fe..c3d99ab1e94 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -10,7 +10,10 @@ from typing import TYPE_CHECKING from sqlalchemy import bindparam from sqlalchemy.ext import baked +from homeassistant.const import PRESSURE_PA, TEMP_CELSIUS import homeassistant.util.dt as dt_util +import homeassistant.util.pressure as pressure_util +import homeassistant.util.temperature as temperature_util from .const import DOMAIN from .models import Statistics, StatisticsMeta, process_timestamp_to_utc_isoformat @@ -42,6 +45,15 @@ QUERY_STATISTIC_META = [ STATISTICS_BAKERY = "recorder_statistics_bakery" STATISTICS_META_BAKERY = "recorder_statistics_bakery" +UNIT_CONVERSIONS = { + PRESSURE_PA: lambda x, units: pressure_util.convert( + x, PRESSURE_PA, units.pressure_unit + ), + TEMP_CELSIUS: lambda x, units: temperature_util.convert( + x, TEMP_CELSIUS, units.temperature_unit + ), +} + _LOGGER = logging.getLogger(__name__) @@ -166,8 +178,8 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None start_time=start_time, end_time=end_time, statistic_ids=statistic_ids ) ) - - return _sorted_statistics_to_dict(stats, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) def get_last_statistics(hass, number_of_stats, statistic_id=None): @@ -193,38 +205,43 @@ def get_last_statistics(hass, number_of_stats, statistic_id=None): ) statistic_ids = [statistic_id] if statistic_id is not None else None - - return _sorted_statistics_to_dict(stats, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) def _sorted_statistics_to_dict( + hass, stats, statistic_ids, + meta_data, ): """Convert SQL results into JSON friendly data structure.""" result = defaultdict(list) + units = hass.config.units + # Set all statistic IDs to empty lists in result set to maintain the order if statistic_ids is not None: for stat_id in statistic_ids: result[stat_id] = [] - # Called in a tight loop so cache the function - # here + # Called in a tight loop so cache the function here _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat - # Append all changes to it + # Append all statistic entries for ent_id, group in groupby(stats, lambda state: state.statistic_id): + unit = meta_data[ent_id]["unit_of_measurement"] + convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) ent_results = result[ent_id] ent_results.extend( { "statistic_id": db_state.statistic_id, "start": _process_timestamp_to_utc_isoformat(db_state.start), - "mean": db_state.mean, - "min": db_state.min, - "max": db_state.max, + "mean": convert(db_state.mean, units), + "min": convert(db_state.min, units), + "max": convert(db_state.max, units), "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), - "state": db_state.state, - "sum": db_state.sum, + "state": convert(db_state.state, units), + "sum": convert(db_state.sum, units), } for db_state in group ) From 9ed93de4724d3a87e5546208a0813243751cb696 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 30 Jun 2021 15:06:33 +0200 Subject: [PATCH 688/750] xknx 0.18.8 (#52340) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 8f46f904466..e075411d8b8 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.7"], + "requirements": ["xknx==0.18.8"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index d0fa388e602..a0e10cdf991 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2393,7 +2393,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.7 +xknx==0.18.8 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 873bfc34ef7..70585ece828 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1311,7 +1311,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.7 +xknx==0.18.8 # homeassistant.components.bluesound # homeassistant.components.fritz From d450cda3858718a1de8045bb9e0345759951e9be Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Jun 2021 16:53:51 +0200 Subject: [PATCH 689/750] Report target unit in statistics meta data (#52341) --- .../components/recorder/statistics.py | 25 +++++- tests/components/history/test_init.py | 80 +++++++++++++------ 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index c3d99ab1e94..753a66926ad 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -48,10 +48,14 @@ STATISTICS_META_BAKERY = "recorder_statistics_bakery" UNIT_CONVERSIONS = { PRESSURE_PA: lambda x, units: pressure_util.convert( x, PRESSURE_PA, units.pressure_unit - ), + ) + if x is not None + else None, TEMP_CELSIUS: lambda x, units: temperature_util.convert( x, TEMP_CELSIUS, units.temperature_unit - ), + ) + if x is not None + else None, } _LOGGER = logging.getLogger(__name__) @@ -133,8 +137,17 @@ def _get_meta_data(hass, session, statistic_ids): return {id: _meta(result, id) for id in statistic_ids} +def _unit_system_unit(unit: str, units) -> str: + if unit == PRESSURE_PA: + return units.pressure_unit + if unit == TEMP_CELSIUS: + return units.temperature_unit + return unit + + def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids.""" + units = hass.config.units with session_scope(hass=hass) as session: baked_query = hass.data[STATISTICS_BAKERY]( lambda session: session.query(*QUERY_STATISTIC_IDS).distinct() @@ -149,8 +162,12 @@ def list_statistic_ids(hass, statistic_type=None): result = execute(baked_query(session)) statistic_ids_list = [statistic_id[0] for statistic_id in result] + statistic_ids = _get_meta_data(hass, session, statistic_ids_list) + for statistic_id in statistic_ids.values(): + unit = _unit_system_unit(statistic_id["unit_of_measurement"], units) + statistic_id["unit_of_measurement"] = unit - return list(_get_meta_data(hass, session, statistic_ids_list).values()) + return list(statistic_ids.values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): @@ -227,7 +244,7 @@ def _sorted_statistics_to_dict( # Called in a tight loop so cache the function here _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat - # Append all statistic entries + # Append all statistic entries, and do unit conversion for ent_id, group in groupby(stats, lambda state: state.statistic_id): unit = meta_data[ent_id]["unit_of_measurement"] convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index ac57069acf0..c4f85717cac 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -14,6 +14,7 @@ import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.common import init_recorder_component from tests.components.recorder.common import trigger_db_commit, wait_recording_done @@ -829,23 +830,46 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien assert response_json[1][0]["entity_id"] == "light.cow" -async def test_statistics_during_period(hass, hass_ws_client): +POWER_SENSOR_ATTRIBUTES = { + "device_class": "power", + "state_class": "measurement", + "unit_of_measurement": "kW", +} +PRESSURE_SENSOR_ATTRIBUTES = { + "device_class": "pressure", + "state_class": "measurement", + "unit_of_measurement": "hPa", +} +TEMPERATURE_SENSOR_ATTRIBUTES = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", +} + + +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period( + hass, hass_ws_client, units, attributes, state, value +): """Test statistics_during_period.""" now = dt_util.utcnow() + hass.config.units = units await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) - hass.states.async_set( - "sensor.test", - 10, - attributes={ - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": "°C", - }, - ) + hass.states.async_set("sensor.test", state, attributes=attributes) await hass.async_block_till_done() await hass.async_add_executor_job(trigger_db_commit, hass) @@ -884,9 +908,9 @@ async def test_statistics_during_period(hass, hass_ws_client): { "statistic_id": "sensor.test", "start": now.isoformat(), - "mean": approx(10.0), - "min": approx(10.0), - "max": approx(10.0), + "mean": approx(value), + "min": approx(value), + "max": approx(value), "last_reset": None, "state": None, "sum": None, @@ -944,23 +968,27 @@ async def test_statistics_during_period_bad_end_time(hass, hass_ws_client): assert response["error"]["code"] == "invalid_end_time" -async def test_list_statistic_ids(hass, hass_ws_client): +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), + ], +) +async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit): """Test list_statistic_ids.""" now = dt_util.utcnow() + hass.config.units = units await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component(hass, "history", {"history": {}}) await async_setup_component(hass, "sensor", {}) await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) - hass.states.async_set( - "sensor.test", - 10, - attributes={ - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": "°C", - }, - ) + hass.states.async_set("sensor.test", 10, attributes=attributes) await hass.async_block_till_done() await hass.async_add_executor_job(trigger_db_commit, hass) @@ -979,7 +1007,7 @@ async def test_list_statistic_ids(hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + {"statistic_id": "sensor.test", "unit_of_measurement": unit} ] await client.send_json( @@ -988,7 +1016,7 @@ async def test_list_statistic_ids(hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + {"statistic_id": "sensor.test", "unit_of_measurement": unit} ] await client.send_json( @@ -997,7 +1025,7 @@ async def test_list_statistic_ids(hass, hass_ws_client): response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": "°C"} + {"statistic_id": "sensor.test", "unit_of_measurement": unit} ] await client.send_json( From 355e557c24077fe1810a0db6188ba646735dbef6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Jun 2021 16:55:41 +0200 Subject: [PATCH 690/750] Bumped version to 2021.7.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 250a50bfada..89fccd434ed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From b565dcf3b0903d5fddb03fec59bc7a5c0d2a5b60 Mon Sep 17 00:00:00 2001 From: Bruce Sheplan Date: Thu, 1 Jul 2021 03:11:01 -0500 Subject: [PATCH 691/750] Add screenlogic reconnect (#52022) Co-authored-by: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> --- .../components/screenlogic/__init__.py | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 521a1ea798c..223ca9262ee 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -40,25 +40,8 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Screenlogic from a config entry.""" - mac = entry.unique_id - # Attempt to re-discover named gateway to follow IP changes - discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] - if mac in discovered_gateways: - connect_info = discovered_gateways[mac] - else: - _LOGGER.warning("Gateway rediscovery failed") - # Static connection defined or fallback from discovery - connect_info = { - SL_GATEWAY_NAME: name_for_mac(mac), - SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS], - SL_GATEWAY_PORT: entry.data[CONF_PORT], - } - try: - gateway = ScreenLogicGateway(**connect_info) - except ScreenLogicError as ex: - _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) - raise ConfigEntryNotReady from ex + gateway = await hass.async_add_executor_job(get_new_gateway, hass, entry) # The api library uses a shared socket connection and does not handle concurrent # requests very well. @@ -99,6 +82,39 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) +def get_connect_info(hass: HomeAssistant, entry: ConfigEntry): + """Construct connect_info from configuration entry and returns it to caller.""" + mac = entry.unique_id + # Attempt to re-discover named gateway to follow IP changes + discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] + if mac in discovered_gateways: + connect_info = discovered_gateways[mac] + else: + _LOGGER.warning("Gateway rediscovery failed") + # Static connection defined or fallback from discovery + connect_info = { + SL_GATEWAY_NAME: name_for_mac(mac), + SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS], + SL_GATEWAY_PORT: entry.data[CONF_PORT], + } + + return connect_info + + +def get_new_gateway(hass: HomeAssistant, entry: ConfigEntry): + """Instantiate a new ScreenLogicGateway, connect to it and return it to caller.""" + + connect_info = get_connect_info(hass, entry) + + try: + gateway = ScreenLogicGateway(**connect_info) + except ScreenLogicError as ex: + _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) + raise ConfigEntryNotReady from ex + + return gateway + + class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage the data update for the Screenlogic component.""" @@ -119,13 +135,32 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): update_interval=interval, ) + def reconnect_gateway(self): + """Instantiate a new ScreenLogicGateway, connect to it and update. Return new gateway to caller.""" + + connect_info = get_connect_info(self.hass, self.config_entry) + + try: + gateway = ScreenLogicGateway(**connect_info) + gateway.update() + except ScreenLogicError as error: + raise UpdateFailed(error) from error + + return gateway + async def _async_update_data(self): """Fetch data from the Screenlogic gateway.""" try: async with self.api_lock: await self.hass.async_add_executor_job(self.gateway.update) except ScreenLogicError as error: - raise UpdateFailed(error) from error + _LOGGER.warning("ScreenLogicError - attempting reconnect: %s", error) + + async with self.api_lock: + self.gateway = await self.hass.async_add_executor_job( + self.reconnect_gateway + ) + return self.gateway.get_data() From 96998aafe388d094df861a2dc80ff13cb476f575 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Jun 2021 11:09:19 -0500 Subject: [PATCH 692/750] Update homekit_controller to use async zeroconf (#52330) --- homeassistant/components/homekit_controller/__init__.py | 6 ++++-- homeassistant/components/homekit_controller/config_flow.py | 6 ++++-- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 44d8286984c..f7507d09837 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -219,8 +219,10 @@ async def async_setup(hass, config): map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() - zeroconf_instance = await zeroconf.async_get_instance(hass) - hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) + async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + hass.data[CONTROLLER] = aiohomekit.Controller( + async_zeroconf_instance=async_zeroconf_instance + ) hass.data[KNOWN_DEVICES] = {} hass.data[TRIGGERS] = {} diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 6ae66d362c9..ebb14e43378 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -99,8 +99,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_setup_controller(self): """Create the controller.""" - zeroconf_instance = await zeroconf.async_get_instance(self.hass) - self.controller = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) + async_zeroconf_instance = await zeroconf.async_get_async_instance(self.hass) + self.controller = aiohomekit.Controller( + async_zeroconf_instance=async_zeroconf_instance + ) async def async_step_user(self, user_input=None): """Handle a flow start.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7ff32e402fe..155f3a4f5f6 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.2.67"], + "requirements": ["aiohomekit==0.4.0"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index a0e10cdf991..13fc6ace1d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.67 +aiohomekit==0.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70585ece828..8e76f627281 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.67 +aiohomekit==0.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 6d346a59c262b32d852d1c991eb09091cc8cc750 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 1 Jul 2021 07:48:29 +0200 Subject: [PATCH 693/750] Bump bt_proximity (#52364) --- homeassistant/components/bluetooth_tracker/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index a41720c2c4f..ccf48a9b8c3 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -2,7 +2,7 @@ "domain": "bluetooth_tracker", "name": "Bluetooth Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", - "requirements": ["bt_proximity==0.2", "pybluez==0.22"], + "requirements": ["bt_proximity==0.2.1", "pybluez==0.22"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 13fc6ace1d2..0c41ce28a7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ brunt==0.1.3 bsblan==0.4.0 # homeassistant.components.bluetooth_tracker -bt_proximity==0.2 +bt_proximity==0.2.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 From c783ef49c72d558172f8bb27aba5078df0ff2be0 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 1 Jul 2021 00:19:22 +0200 Subject: [PATCH 694/750] Bump pyatmo to v5.2.0 (#52365) * Bump pyatmo to v5.2.0 * Revert formatting changes --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index a6630a00f50..de7fbc36038 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==5.1.0" + "pyatmo==5.2.0" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 0c41ce28a7b..674956249fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1319,7 +1319,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.1.0 +pyatmo==5.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e76f627281..6ca5677143e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==5.1.0 +pyatmo==5.2.0 # homeassistant.components.apple_tv pyatv==0.7.7 From 8de7312c92469f73fc30a10e5396c60a921bd44a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 1 Jul 2021 14:05:55 -0400 Subject: [PATCH 695/750] Bump up ZHA dependencies (#52374) * Bump up ZHA dependencies * Fix broken tests * Update tests/components/zha/common.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/common.py | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d1e79d1b67b..b366b73d6c8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,10 +7,10 @@ "bellows==0.25.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.57", + "zha-quirks==0.0.58", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.34.0", + "zigpy==0.35.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index 674956249fb..2ea8a88b303 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2431,7 +2431,7 @@ zengge==0.2 zeroconf==0.32.0 # homeassistant.components.zha -zha-quirks==0.0.57 +zha-quirks==0.0.58 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2455,7 +2455,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.34.0 +zigpy==0.35.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ca5677143e..aa044b66df5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,7 +1334,7 @@ zeep[async]==4.0.0 zeroconf==0.32.0 # homeassistant.components.zha -zha-quirks==0.0.57 +zha-quirks==0.0.58 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1352,7 +1352,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.34.0 +zigpy==0.35.0 # homeassistant.components.zwave_js zwave-js-server-python==0.27.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 45caed95ae6..eb65cc4fd2e 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -40,8 +40,9 @@ class FakeEndpoint: if _patch_cluster: patch_cluster(cluster) self.in_clusters[cluster_id] = cluster - if hasattr(cluster, "ep_attribute"): - setattr(self, cluster.ep_attribute, cluster) + ep_attribute = cluster.ep_attribute + if ep_attribute: + setattr(self, ep_attribute, cluster) def add_output_cluster(self, cluster_id, _patch_cluster=True): """Add an output cluster.""" From 5cc878fc793ce33302c139b6c83f872bc2b6c698 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 1 Jul 2021 03:35:14 -0500 Subject: [PATCH 696/750] Fix missing default latitude/longitude/elevation in OpenUV config flow (#52380) --- .../components/openuv/config_flow.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 2ed6b56d914..e31cef9ee0a 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -14,26 +14,35 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude, - vol.Optional(CONF_ELEVATION): vol.Coerce(float), - } -) - class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 + @property + def config_schema(self): + """Return the config schema.""" + return vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Inclusive( + CONF_LATITUDE, "coords", default=self.hass.config.latitude + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coords", default=self.hass.config.longitude + ): cv.longitude, + vol.Optional( + CONF_ELEVATION, default=self.hass.config.elevation + ): vol.Coerce(float), + } + ) + async def _show_form(self, errors=None): """Show the form to the user.""" return self.async_show_form( step_id="user", - data_schema=CONFIG_SCHEMA, + data_schema=self.config_schema, errors=errors if errors else {}, ) From 3dcad64d53ad1a79504cf1ce1e75e3dd35f4f589 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 09:51:47 +0200 Subject: [PATCH 697/750] Improve sensor statistics tests (#52386) --- .../components/recorder/statistics.py | 20 +- homeassistant/components/sensor/recorder.py | 8 + tests/components/sensor/test_recorder.py | 532 +++++++++++++----- 3 files changed, 403 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 753a66926ad..0e01005c13a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -45,6 +45,8 @@ QUERY_STATISTIC_META = [ STATISTICS_BAKERY = "recorder_statistics_bakery" STATISTICS_META_BAKERY = "recorder_statistics_bakery" +# Convert pressure and temperature statistics from the native unit used for statistics +# to the units configured by the user UNIT_CONVERSIONS = { PRESSURE_PA: lambda x, units: pressure_util.convert( x, PRESSURE_PA, units.pressure_unit @@ -137,7 +139,8 @@ def _get_meta_data(hass, session, statistic_ids): return {id: _meta(result, id) for id in statistic_ids} -def _unit_system_unit(unit: str, units) -> str: +def _configured_unit(unit: str, units) -> str: + """Return the pressure and temperature units configured by the user.""" if unit == PRESSURE_PA: return units.pressure_unit if unit == TEMP_CELSIUS: @@ -146,7 +149,7 @@ def _unit_system_unit(unit: str, units) -> str: def list_statistic_ids(hass, statistic_type=None): - """Return statistic_ids.""" + """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: baked_query = hass.data[STATISTICS_BAKERY]( @@ -161,13 +164,14 @@ def list_statistic_ids(hass, statistic_type=None): baked_query += lambda q: q.order_by(Statistics.statistic_id) result = execute(baked_query(session)) - statistic_ids_list = [statistic_id[0] for statistic_id in result] - statistic_ids = _get_meta_data(hass, session, statistic_ids_list) - for statistic_id in statistic_ids.values(): - unit = _unit_system_unit(statistic_id["unit_of_measurement"], units) - statistic_id["unit_of_measurement"] = unit - return list(statistic_ids.values()) + statistic_ids = [statistic_id[0] for statistic_id in result] + meta_data = _get_meta_data(hass, session, statistic_ids) + for item in meta_data.values(): + unit = _configured_unit(item["unit_of_measurement"], units) + item["unit_of_measurement"] = unit + + return list(meta_data.values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index ac26e06e07d..f88b91d1b70 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -54,6 +54,7 @@ DEVICE_CLASS_STATISTICS = { DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, } +# Normalized units which will be stored in the statistics table DEVICE_CLASS_UNITS = { DEVICE_CLASS_ENERGY: ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER: POWER_WATT, @@ -62,14 +63,18 @@ DEVICE_CLASS_UNITS = { } UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { + # Convert energy to kWh DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, ENERGY_WATT_HOUR: lambda x: x / 1000, }, + # Convert power W DEVICE_CLASS_POWER: { POWER_WATT: lambda x: x, POWER_KILO_WATT: lambda x: x * 1000, }, + # Convert pressure to Pa + # Note: pressure_util.convert is bypassed to avoid redundant error checking DEVICE_CLASS_PRESSURE: { PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR], PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA], @@ -78,6 +83,8 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], }, + # Convert temperature to °C + # Note: temperature_util.convert is bypassed to avoid redundant error checking DEVICE_CLASS_TEMPERATURE: { TEMP_CELSIUS: lambda x: x, TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius, @@ -85,6 +92,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { }, } +# Keep track of entities for which a warning about unsupported unit has been logged WARN_UNSUPPORTED_UNIT = set() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 65346f1feba..99ede396381 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,32 +1,141 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta -from unittest.mock import patch, sentinel +from unittest.mock import patch +import pytest from pytest import approx from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat -from homeassistant.components.recorder.statistics import statistics_during_period -from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS +from homeassistant.components.recorder.statistics import ( + list_statistic_ids, + statistics_during_period, +) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.components.recorder.common import wait_recording_done +ENERGY_SENSOR_ATTRIBUTES = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", +} +POWER_SENSOR_ATTRIBUTES = { + "device_class": "power", + "state_class": "measurement", + "unit_of_measurement": "kW", +} +PRESSURE_SENSOR_ATTRIBUTES = { + "device_class": "pressure", + "state_class": "measurement", + "unit_of_measurement": "hPa", +} +TEMPERATURE_SENSOR_ATTRIBUTES = { + "device_class": "temperature", + "state_class": "measurement", + "unit_of_measurement": "°C", +} -def test_compile_hourly_statistics(hass_recorder): + +@pytest.mark.parametrize( + "device_class,unit,native_unit,mean,min,max", + [ + ("battery", "%", "%", 16.440677, 10, 30), + ("battery", None, None, 16.440677, 10, 30), + ("humidity", "%", "%", 16.440677, 10, 30), + ("humidity", None, None, 16.440677, 10, 30), + ("pressure", "Pa", "Pa", 16.440677, 10, 30), + ("pressure", "hPa", "Pa", 1644.0677, 1000, 3000), + ("pressure", "mbar", "Pa", 1644.0677, 1000, 3000), + ("pressure", "inHg", "Pa", 55674.53, 33863.89, 101591.67), + ("pressure", "psi", "Pa", 113354.48, 68947.57, 206842.71), + ("temperature", "°C", "°C", 16.440677, 10, 30), + ("temperature", "°F", "°C", -8.644068, -12.22222, -1.111111), + ], +) +def test_compile_hourly_statistics( + hass_recorder, caplog, device_class, unit, native_unit, mean, min, max +): """Test compiling hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) recorder.do_adhoc_statistics(period="hourly", start=zero) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize("attributes", [TEMPERATURE_SENSOR_ATTRIBUTES]) +def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes): + """Test compiling hourly statistics for unsupported sensor.""" + attributes = dict(attributes) + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + four, states = record_states(hass, zero, "sensor.test1", attributes) + if "unit_of_measurement" in attributes: + attributes["unit_of_measurement"] = "invalid" + _, _states = record_states(hass, zero, "sensor.test2", attributes) + states = {**states, **_states} + attributes.pop("unit_of_measurement") + _, _states = record_states(hass, zero, "sensor.test3", attributes) + states = {**states, **_states} + attributes["state_class"] = "invalid" + _, _states = record_states(hass, zero, "sensor.test4", attributes) + states = {**states, **_states} + attributes.pop("state_class") + _, _states = record_states(hass, zero, "sensor.test5", attributes) + states = {**states, **_states} + attributes["state_class"] = "measurement" + _, _states = record_states(hass, zero, "sensor.test6", attributes) + states = {**states, **_states} + attributes["state_class"] = "unsupported" + _, _states = record_states(hass, zero, "sensor.test7", attributes) + states = {**states, **_states} + + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "°C"} + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -42,23 +151,36 @@ def test_compile_hourly_statistics(hass_recorder): } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,native_unit,factor", + [ + ("energy", "kWh", "kWh", 1), + ("energy", "Wh", "kWh", 1 / 1000), + ("monetary", "€", "€", 1), + ("monetary", "SEK", "SEK", 1), + ], +) +def test_compile_hourly_energy_statistics( + hass_recorder, caplog, device_class, unit, native_unit, factor +): """Test compiling hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = { - "device_class": "energy", + attributes = { + "device_class": device_class, "state_class": "measurement", - "unit_of_measurement": "kWh", + "unit_of_measurement": unit, + "last_reset": None, } - sns2_attr = {"device_class": "energy"} - sns3_attr = {} + seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - zero, four, eight, states = record_energy_states( - hass, sns1_attr, sns2_attr, sns3_attr + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", attributes, seq ) hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution @@ -71,6 +193,97 @@ def test_compile_hourly_energy_statistics(hass_recorder): wait_recording_done(hass) recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + ] + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(factor * seq[2]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(factor * seq[5]), + "sum": approx(factor * 10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(factor * seq[8]), + "sum": approx(factor * 40.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): + """Test compiling hourly statistics.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + "last_reset": None, + } + sns2_attr = {"device_class": "energy"} + sns3_attr = {} + sns4_attr = { + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "kWh", + } + seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] + seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] + seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", sns1_attr, seq1 + ) + _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + states = {**states, **_states} + + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"} + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -106,32 +319,37 @@ def test_compile_hourly_energy_statistics(hass_recorder): }, ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_energy_statistics2(hass_recorder): - """Test compiling hourly statistics.""" +def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): + """Test compiling multiple hourly statistics.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - sns1_attr = { - "device_class": "energy", - "state_class": "measurement", - "unit_of_measurement": "kWh", - } - sns2_attr = { - "device_class": "energy", - "state_class": "measurement", - "unit_of_measurement": "kWh", - } + sns1_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} + sns2_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} sns3_attr = { - "device_class": "energy", - "state_class": "measurement", + **ENERGY_SENSOR_ATTRIBUTES, "unit_of_measurement": "Wh", + "last_reset": None, } + sns4_attr = {**ENERGY_SENSOR_ATTRIBUTES} + seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] + seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] + seq3 = [0, 0, 5, 10, 30, 50, 60, 80, 90] + seq4 = [0, 0, 5, 10, 30, 50, 60, 80, 90] - zero, four, eight, states = record_energy_states( - hass, sns1_attr, sns2_attr, sns3_attr + four, eight, states = record_energy_states( + hass, zero, "sensor.test1", sns1_attr, seq1 ) + _, _, _states = record_energy_states(hass, zero, "sensor.test2", sns2_attr, seq2) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test3", sns3_attr, seq3) + states = {**states, **_states} + _, _, _states = record_energy_states(hass, zero, "sensor.test4", sns4_attr, seq4) + states = {**states, **_states} hist = history.get_significant_states( hass, zero - timedelta.resolution, eight + timedelta.resolution ) @@ -143,6 +361,12 @@ def test_compile_hourly_energy_statistics2(hass_recorder): wait_recording_done(hass) recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) wait_recording_done(hass) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}, + {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"}, + {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"}, + ] stats = statistics_during_period(hass, zero) assert stats == { "sensor.test1": [ @@ -242,14 +466,39 @@ def test_compile_hourly_energy_statistics2(hass_recorder): }, ], } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_unchanged(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,value", + [ + ("battery", "%", 30), + ("battery", None, 30), + ("humidity", "%", 30), + ("humidity", None, 30), + ("pressure", "Pa", 30), + ("pressure", "hPa", 3000), + ("pressure", "mbar", 3000), + ("pressure", "inHg", 101591.67), + ("pressure", "psi", 206842.71), + ("temperature", "°C", 30), + ("temperature", "°F", -1.111111), + ], +) +def test_compile_hourly_statistics_unchanged( + hass_recorder, caplog, device_class, unit, value +): """Test compiling hourly statistics, with no changes during the hour.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) @@ -261,23 +510,27 @@ def test_compile_hourly_statistics_unchanged(hass_recorder): { "statistic_id": "sensor.test1", "start": process_timestamp_to_utc_isoformat(four), - "mean": approx(30.0), - "min": approx(30.0), - "max": approx(30.0), + "mean": approx(value), + "min": approx(value), + "max": approx(value), "last_reset": None, "state": None, "sum": None, } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_partially_unavailable(hass_recorder): +def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): """Test compiling hourly statistics, with the sensor being partially unavailable.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states_partially_unavailable(hass) + four, states = record_states_partially_unavailable( + hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES + ) hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) @@ -298,39 +551,87 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder): } ] } + assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_hourly_statistics_unavailable(hass_recorder): +@pytest.mark.parametrize( + "device_class,unit,value", + [ + ("battery", "%", 30), + ("battery", None, 30), + ("humidity", "%", 30), + ("humidity", None, 30), + ("pressure", "Pa", 30), + ("pressure", "hPa", 3000), + ("pressure", "mbar", 3000), + ("pressure", "inHg", 101591.67), + ("pressure", "psi", 206842.71), + ("temperature", "°C", 30), + ("temperature", "°F", -1.111111), + ], +) +def test_compile_hourly_statistics_unavailable( + hass_recorder, caplog, device_class, unit, value +): """Test compiling hourly statistics, with the sensor being unavailable.""" + zero = dt_util.utcnow() hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) - zero, four, states = record_states_partially_unavailable(hass) + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": unit, + } + four, states = record_states_partially_unavailable( + hass, zero, "sensor.test1", attributes + ) + _, _states = record_states(hass, zero, "sensor.test2", attributes) + states = {**states, **_states} hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) recorder.do_adhoc_statistics(period="hourly", start=four) wait_recording_done(hass) stats = statistics_during_period(hass, four) - assert stats == {} + assert stats == { + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(four), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text -def record_states(hass): +def test_compile_hourly_statistics_fails(hass_recorder, caplog): + """Test compiling hourly statistics throws.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + with patch( + "homeassistant.components.sensor.recorder.compile_statistics", + side_effect=Exception, + ): + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + assert "Error while processing event StatisticsTask" in caplog.text + + +def record_states(hass, zero, entity_id, attributes): """Record some test states. We inject a bunch of state updates for temperature sensors. """ - mp = "media_player.test" - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns1_attr = { - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, - } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} + attributes = dict(attributes) def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -338,46 +639,29 @@ def record_states(hass): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=1) two = one + timedelta(minutes=10) three = two + timedelta(minutes=40) four = three + timedelta(minutes=10) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "10", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "15", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "30", attributes=attributes)) - return zero, four, states + return four, states -def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): +def record_energy_states(hass, zero, entity_id, _attributes, seq): """Record some test states. We inject a bunch of state updates for energy sensors. """ - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns4 = "sensor.test4" def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -385,7 +669,6 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=15) two = one + timedelta(minutes=30) three = two + timedelta(minutes=15) @@ -395,88 +678,50 @@ def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): seven = six + timedelta(minutes=15) eight = seven + timedelta(minutes=30) - sns1_attr = {**_sns1_attr, "last_reset": zero.isoformat()} - sns2_attr = {**_sns2_attr, "last_reset": zero.isoformat()} - sns3_attr = {**_sns3_attr, "last_reset": zero.isoformat()} - sns4_attr = {**_sns3_attr} + attributes = dict(_attributes) + if "last_reset" in _attributes: + attributes["last_reset"] = zero.isoformat() - states = {sns1: [], sns2: [], sns3: [], sns4: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "110", attributes=sns2_attr)) # Sum 0 - states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 - states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) # Sum 5 - states[sns2].append(set_state(sns2, "120", attributes=sns2_attr)) # Sum 10 - states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 - states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[1], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) # Sum 10 - states[sns2].append(set_state(sns2, "130", attributes=sns2_attr)) # Sum 20 - states[sns3].append(set_state(sns3, "5", attributes=sns3_attr)) # Sum 5 - states[sns4].append(set_state(sns4, "5", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[2], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "0", attributes=sns2_attr)) # Sum -110 - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) # Sum 10 - states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[3], attributes=attributes)) - sns1_attr = {**_sns1_attr, "last_reset": four.isoformat()} - sns2_attr = {**_sns2_attr, "last_reset": four.isoformat()} - sns3_attr = {**_sns3_attr, "last_reset": four.isoformat()} + attributes = dict(_attributes) + if "last_reset" in _attributes: + attributes["last_reset"] = four.isoformat() with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): - states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) # Sum 0 - states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) # Sum -110 - states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) # Sum 10 - states[sns4].append(set_state(sns4, "30", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[4], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five): - states[sns1].append(set_state(sns1, "40", attributes=sns1_attr)) # Sum 10 - states[sns2].append(set_state(sns2, "45", attributes=sns2_attr)) # Sum -95 - states[sns3].append(set_state(sns3, "50", attributes=sns3_attr)) # Sum 30 - states[sns4].append(set_state(sns4, "50", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[5], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six): - states[sns1].append(set_state(sns1, "50", attributes=sns1_attr)) # Sum 20 - states[sns2].append(set_state(sns2, "55", attributes=sns2_attr)) # Sum -85 - states[sns3].append(set_state(sns3, "60", attributes=sns3_attr)) # Sum 40 - states[sns4].append(set_state(sns4, "60", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[6], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven): - states[sns1].append(set_state(sns1, "60", attributes=sns1_attr)) # Sum 30 - states[sns2].append(set_state(sns2, "65", attributes=sns2_attr)) # Sum -75 - states[sns3].append(set_state(sns3, "80", attributes=sns3_attr)) # Sum 60 - states[sns4].append(set_state(sns4, "80", attributes=sns4_attr)) # - + states[entity_id].append(set_state(entity_id, seq[7], attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight): - states[sns1].append(set_state(sns1, "70", attributes=sns1_attr)) # Sum 40 - states[sns2].append(set_state(sns2, "75", attributes=sns2_attr)) # Sum -65 - states[sns3].append(set_state(sns3, "90", attributes=sns3_attr)) # Sum 70 + states[entity_id].append(set_state(entity_id, seq[8], attributes=attributes)) - return zero, four, eight, states + return four, eight, states -def record_states_partially_unavailable(hass): +def record_states_partially_unavailable(hass, zero, entity_id, attributes): """Record some test states. We inject a bunch of state updates temperature sensors. """ - mp = "media_player.test" - sns1 = "sensor.test1" - sns2 = "sensor.test2" - sns3 = "sensor.test3" - sns1_attr = { - "device_class": "temperature", - "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, - } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -484,32 +729,21 @@ def record_states_partially_unavailable(hass): wait_recording_done(hass) return hass.states.get(entity_id) - zero = dt_util.utcnow() one = zero + timedelta(minutes=1) two = one + timedelta(minutes=15) three = two + timedelta(minutes=30) four = three + timedelta(minutes=15) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {entity_id: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): - states[mp].append( - set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) - ) - states[mp].append( - set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) - ) - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "10", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): - states[sns1].append(set_state(sns1, "25", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "25", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "25", attributes=sns3_attr)) + states[entity_id].append(set_state(entity_id, "25", attributes=attributes)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): - states[sns1].append(set_state(sns1, STATE_UNAVAILABLE, attributes=sns1_attr)) - states[sns2].append(set_state(sns2, STATE_UNAVAILABLE, attributes=sns2_attr)) - states[sns3].append(set_state(sns3, STATE_UNAVAILABLE, attributes=sns3_attr)) + states[entity_id].append( + set_state(entity_id, STATE_UNAVAILABLE, attributes=attributes) + ) - return zero, four, states + return four, states From b8b0bc9392bd5c27935ccc4144d331c3ebedb3bf Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 2 Jul 2021 11:49:42 +0200 Subject: [PATCH 698/750] Reject trusted network access from proxies (#52388) --- .../auth/providers/trusted_networks.py | 14 ++++++++ homeassistant/components/http/forwarded.py | 8 ++--- tests/auth/providers/test_trusted_networks.py | 25 ++++++++++++++ tests/components/http/test_forwarded.py | 33 ++++--------------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index fd2014667f8..7b609f371ef 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -81,6 +81,17 @@ class TrustedNetworksAuthProvider(AuthProvider): """Return trusted users per network.""" return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) + @property + def trusted_proxies(self) -> list[IPNetwork]: + """Return trusted proxies in the system.""" + if not self.hass.http: + return [] + + return [ + ip_network(trusted_proxy) + for trusted_proxy in self.hass.http.trusted_proxies + ] + @property def support_mfa(self) -> bool: """Trusted Networks auth provider does not support MFA.""" @@ -178,6 +189,9 @@ class TrustedNetworksAuthProvider(AuthProvider): ): raise InvalidAuthError("Not in trusted_networks") + if any(ip_addr in trusted_proxy for trusted_proxy in self.trusted_proxies): + raise InvalidAuthError("Can't allow access from a proxy server") + @callback def async_validate_refresh_token( self, refresh_token: RefreshToken, remote_ip: str | None = None diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 684dbbb9e2b..18bc51af1d1 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -129,11 +129,9 @@ def async_setup_forwarded( overrides["remote"] = str(forwarded_ip) break else: - _LOGGER.warning( - "Request originated directly from a trusted proxy included in X-Forwarded-For: %s, this is likely a miss configuration and will be rejected", - forwarded_for_headers, - ) - raise HTTPBadRequest() + # If all the IP addresses are from trusted networks, take the left-most. + forwarded_for_index = -1 + overrides["remote"] = str(forwarded_for[-1]) # Handle X-Forwarded-Proto forwarded_proto_headers: list[str] = request.headers.getall( diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 412f660adc3..d7574bf0da1 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -8,7 +8,9 @@ import voluptuous as vol from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.setup import async_setup_component @pytest.fixture @@ -144,6 +146,29 @@ async def test_validate_access(provider): provider.async_validate_access(ip_address("2001:db8::ff00:42:8329")) +async def test_validate_access_proxy(hass, provider): + """Test validate access from trusted networks are blocked from proxy.""" + + await async_setup_component( + hass, + "http", + { + "http": { + CONF_TRUSTED_PROXIES: ["192.168.128.0/31", "fd00::1"], + CONF_USE_X_FORWARDED_FOR: True, + } + }, + ) + provider.async_validate_access(ip_address("192.168.128.2")) + provider.async_validate_access(ip_address("fd00::2")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.0")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("192.168.128.1")) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address("fd00::1")) + + async def test_validate_refresh_token(provider): """Verify re-validation of refresh token.""" with patch.object(provider, "async_validate_access") as mock: diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 8d8467b699f..400a1f32729 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -43,9 +43,15 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): @pytest.mark.parametrize( "trusted_proxies,x_forwarded_for,remote", [ + ( + ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], + "10.10.10.10, 1.1.1.1", + "10.10.10.10", + ), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123,2.2.2.2,1.1.1.1", "2.2.2.2"), (["127.0.0.0/24"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "1.1.1.1"), + (["127.0.0.0/24"], "127.0.0.1", "127.0.0.1"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 1.1.1.1", "123.123.123.123"), (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), (["127.0.0.1"], "255.255.255.255", "255.255.255.255"), @@ -77,33 +83,6 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 -@pytest.mark.parametrize( - "trusted_proxies,x_forwarded_for", - [ - ( - ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], - "10.10.10.10, 1.1.1.1", - ), - (["127.0.0.0/24"], "127.0.0.1"), - ], -) -async def test_x_forwarded_for_from_trusted_proxy_rejected( - trusted_proxies, x_forwarded_for, aiohttp_client -): - """Test that we reject forwarded requests from proxy server itself.""" - - app = web.Application() - app.router.add_get("/", mock_handler) - async_setup_forwarded( - app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] - ) - - mock_api_client = await aiohttp_client(app) - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) - - assert resp.status == 400 - - async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): """Test that we warn when processing is disabled, but proxy has been detected.""" From eea544d2d23a51bf91e907feb135ae617bb3876f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jul 2021 17:34:59 +0200 Subject: [PATCH 699/750] Fix MQTT cover optimistic mode (#52392) * Fix MQTT cover optimistic mode * Add test --- homeassistant/components/mqtt/cover.py | 52 +++++++++++++++++++------- tests/components/mqtt/test_cover.py | 28 ++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index fd3e36c04e1..d920d12662f 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -245,11 +245,45 @@ class MqttCover(MqttEntity, CoverEntity): return PLATFORM_SCHEMA def _setup_from_config(self, config): - self._optimistic = config[CONF_OPTIMISTIC] or ( - config.get(CONF_STATE_TOPIC) is None + no_position = ( + config.get(CONF_SET_POSITION_TOPIC) is None and config.get(CONF_GET_POSITION_TOPIC) is None ) - self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC] + no_state = ( + config.get(CONF_COMMAND_TOPIC) is None + and config.get(CONF_STATE_TOPIC) is None + ) + no_tilt = ( + config.get(CONF_TILT_COMMAND_TOPIC) is None + and config.get(CONF_TILT_STATUS_TOPIC) is None + ) + optimistic_position = ( + config.get(CONF_SET_POSITION_TOPIC) is not None + and config.get(CONF_GET_POSITION_TOPIC) is None + ) + optimistic_state = ( + config.get(CONF_COMMAND_TOPIC) is not None + and config.get(CONF_STATE_TOPIC) is None + ) + optimistic_tilt = ( + config.get(CONF_TILT_COMMAND_TOPIC) is not None + and config.get(CONF_TILT_STATUS_TOPIC) is None + ) + + if config[CONF_OPTIMISTIC] or ( + (no_position or optimistic_position) + and (no_state or optimistic_state) + and (no_tilt or optimistic_tilt) + ): + # Force into optimistic mode. + self._optimistic = True + + if ( + config[CONF_TILT_STATE_OPTIMISTIC] + or config.get(CONF_TILT_STATUS_TOPIC) is None + ): + # Force into optimistic tilt mode. + self._tilt_optimistic = True value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: @@ -418,17 +452,7 @@ class MqttCover(MqttEntity, CoverEntity): "qos": self._config[CONF_QOS], } - if ( - self._config.get(CONF_GET_POSITION_TOPIC) is None - and self._config.get(CONF_STATE_TOPIC) is None - ): - # Force into optimistic mode. - self._optimistic = True - - if self._config.get(CONF_TILT_STATUS_TOPIC) is None: - # Force into optimistic tilt mode. - self._tilt_optimistic = True - else: + if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 7d763895428..0ee2557fbd6 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -393,6 +393,34 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): assert current_cover_position == 20 +@pytest.mark.parametrize( + "config, assumed_state", + [ + ({"command_topic": "abc"}, True), + ({"command_topic": "abc", "state_topic": "abc"}, False), + # ({"set_position_topic": "abc"}, True), - not a valid configuration + ({"set_position_topic": "abc", "position_topic": "abc"}, False), + ({"tilt_command_topic": "abc"}, True), + ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False), + ], +) +async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): + """Test assumed_state is set correctly.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + if assumed_state: + assert ATTR_ASSUMED_STATE in state.attributes + else: + assert ATTR_ASSUMED_STATE not in state.attributes + + async def test_optimistic_state_change(hass, mqtt_mock): """Test changing state optimistically.""" assert await async_setup_component( From 4959561bde56d836a6f2c00d7974c696f44d26d7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Jul 2021 14:53:03 +0200 Subject: [PATCH 700/750] Fix sensor statistics collection with empty states (#52393) --- homeassistant/components/sensor/recorder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index f88b91d1b70..d9200bdf797 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -161,13 +161,15 @@ def _normalize_states( entity_history: list[State], device_class: str, entity_id: str ) -> tuple[str | None, list[tuple[float, State]]]: """Normalize units.""" + unit = None if device_class not in UNIT_CONVERSIONS: # We're not normalizing this device class, return the state as they are fstates = [ (float(el.state), el) for el in entity_history if _is_number(el.state) ] - unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if fstates: + unit = fstates[0][1].attributes.get(ATTR_UNIT_OF_MEASUREMENT) return unit, fstates fstates = [] From 61bc95d70469f4ab4a1f883e02a160e4380fab90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Thu, 1 Jul 2021 21:32:46 +0200 Subject: [PATCH 701/750] Bump pysma to 0.6.1 (#52401) --- homeassistant/components/sma/__init__.py | 2 +- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 0df3ef8cb7c..2eb0e6760ed 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -167,7 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: pysma.exceptions.SmaReadException, pysma.exceptions.SmaConnectionException, ) as exc: - raise UpdateFailed from exc + raise UpdateFailed(exc) from exc interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 721431b89a7..85f6de7cb7c 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.0"], + "requirements": ["pysma==0.6.1"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2ea8a88b303..08fafae4050 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1756,7 +1756,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.0 +pysma==0.6.1 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa044b66df5..c825c050adf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.0 +pysma==0.6.1 # homeassistant.components.smappee pysmappee==0.2.25 From 527af96ad965a2f6869389bcb84243882c13a930 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Fri, 2 Jul 2021 10:15:05 +0100 Subject: [PATCH 702/750] Add update listener to Coinbase (#52404) Co-authored-by: Franck Nijhof --- homeassistant/components/coinbase/__init__.py | 26 +++++++++++++++++ homeassistant/components/coinbase/sensor.py | 16 +++++++---- tests/components/coinbase/const.py | 4 +-- tests/components/coinbase/test_config_flow.py | 28 ++++++++++++++++--- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index eb4370a9534..08b97756dff 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -70,6 +71,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_and_update_instance, entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN] ) + entry.async_on_unload(entry.add_update_listener(update_listener)) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = instance @@ -96,6 +99,29 @@ def create_and_update_instance(api_key, api_token): return instance +async def update_listener(hass, config_entry): + """Handle options update.""" + + await hass.config_entries.async_reload(config_entry.entry_id) + + registry = entity_registry.async_get(hass) + entities = entity_registry.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + + # Remove orphaned entities + for entity in entities: + currency = entity.unique_id.split("-")[-1] + if "xe" in entity.unique_id and currency not in config_entry.options.get( + CONF_EXCHANGE_RATES + ): + registry.async_remove(entity.entity_id) + elif "wallet" in entity.unique_id and currency not in config_entry.options.get( + CONF_CURRENCIES + ): + registry.async_remove(entity.entity_id) + + def get_accounts(client): """Handle paginated accounts.""" response = client.get_accounts() diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 5febfe8a978..13981619051 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -11,6 +11,7 @@ from .const import ( API_ACCOUNT_ID, API_ACCOUNT_NAME, API_ACCOUNT_NATIVE_BALANCE, + API_RATES, CONF_CURRENCIES, CONF_EXCHANGE_RATES, DOMAIN, @@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if CONF_CURRENCIES in config_entry.options: desired_currencies = config_entry.options[CONF_CURRENCIES] - exchange_native_currency = instance.exchange_rates.currency + exchange_native_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY] for currency in desired_currencies: if currency not in provided_currencies: @@ -81,9 +82,12 @@ class AccountSensor(SensorEntity): self._coinbase_data = coinbase_data self._currency = currency for account in coinbase_data.accounts: - if account.currency == currency: + if account[API_ACCOUNT_CURRENCY] == currency: self._name = f"Coinbase {account[API_ACCOUNT_NAME]}" - self._id = f"coinbase-{account[API_ACCOUNT_ID]}" + self._id = ( + f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" + f"{account[API_ACCOUNT_CURRENCY]}" + ) self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._unit_of_measurement = account[API_ACCOUNT_CURRENCY] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ @@ -131,7 +135,7 @@ class AccountSensor(SensorEntity): """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: - if account.currency == self._currency: + if account[API_ACCOUNT_CURRENCY] == self._currency: self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ API_ACCOUNT_AMOUNT @@ -150,9 +154,9 @@ class ExchangeRateSensor(SensorEntity): self._coinbase_data = coinbase_data self.currency = exchange_currency self._name = f"{exchange_currency} Exchange Rate" - self._id = f"{coinbase_data.user_id}-xe-{exchange_currency}" + self._id = f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}" self._state = round( - 1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2 + 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = native_currency diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 52505be514e..864ebc18701 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -11,14 +11,14 @@ BAD_EXCHANGE_RATE = "ETH" MOCK_ACCOUNTS_RESPONSE = [ { "balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3}, - "currency": "BTC", + "currency": GOOD_CURRENCY_3, "id": "ABCDEF", "name": "BTC Wallet", "native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2}, }, { "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY}, - "currency": "BTC", + "currency": GOOD_CURRENCY, "id": "123456789", "name": "BTC Wallet", "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index dc036d23a6f..4c7b6c13333 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -19,7 +19,14 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE +from .const import ( + BAD_CURRENCY, + BAD_EXCHANGE_RATE, + GOOD_CURRENCY, + GOOD_CURRENCY_2, + GOOD_EXCHNAGE_RATE, + GOOD_EXCHNAGE_RATE_2, +) from tests.common import MockConfigEntry @@ -139,6 +146,18 @@ async def test_form_catch_all_exception(hass): async def test_option_good_account_currency(hass): """Test we handle a good wallet currency option.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + title="Test User", + data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, + options={ + CONF_CURRENCIES: [GOOD_CURRENCY_2], + CONF_EXCHANGE_RATES: [], + }, + ) + config_entry.add_to_hass(hass) + with patch( "coinbase.wallet.client.Client.get_current_user", return_value=mock_get_current_user(), @@ -148,7 +167,8 @@ async def test_option_good_account_currency(hass): "coinbase.wallet.client.Client.get_exchange_rates", return_value=mock_get_exchange_rates(), ): - config_entry = await init_mock_coinbase(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() result2 = await hass.config_entries.options.async_configure( @@ -191,12 +211,12 @@ async def test_option_good_exchange_rate(hass): """Test we handle a good exchange rate option.""" config_entry = MockConfigEntry( domain=DOMAIN, - unique_id="abcde12345", + entry_id="abcde12345", title="Test User", data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, options={ CONF_CURRENCIES: [], - CONF_EXCHANGE_RATES: [], + CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE_2], }, ) config_entry.add_to_hass(hass) From e8ed4979500a28a0a4d5459be2d813e42f1f0ddc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 00:30:33 +0200 Subject: [PATCH 703/750] Upgrade wled to 0.7.1 (#52405) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 237f6850b66..348109f6b87 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.7.0"], + "requirements": ["wled==0.7.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 08fafae4050..87486dceef3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2378,7 +2378,7 @@ wirelesstagpy==0.4.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.0 +wled==0.7.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c825c050adf..f79b8764e58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1302,7 +1302,7 @@ wiffi==1.0.1 withings-api==2.3.2 # homeassistant.components.wled -wled==0.7.0 +wled==0.7.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 From e2e72851d79db535c2392347bc2133286d8414ab Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Jul 2021 01:36:48 -0400 Subject: [PATCH 704/750] Bump eight sleep dependency to fix bug (#52408) --- .../components/eight_sleep/__init__.py | 27 ++++++++----------- .../components/eight_sleep/binary_sensor.py | 14 +++++----- .../components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 67c195da3e6..4e16cd1087f 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -11,10 +11,10 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SENSORS, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -29,7 +29,6 @@ _LOGGER = logging.getLogger(__name__) CONF_PARTNER = "partner" DATA_EIGHT = "eight_sleep" -DEFAULT_PARTNER = False DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" @@ -86,12 +85,15 @@ SERVICE_EIGHT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER, default=DEFAULT_PARTNER): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_PARTNER), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PARTNER): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -104,7 +106,6 @@ async def async_setup(hass, config): conf = config.get(DOMAIN) user = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) - partner = conf.get(CONF_PARTNER) if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant") @@ -112,7 +113,7 @@ async def async_setup(hass, config): timezone = str(hass.config.time_zone) - eight = EightSleep(user, password, timezone, partner, None, hass.loop) + eight = EightSleep(user, password, timezone, async_get_clientsession(hass)) hass.data[DATA_EIGHT] = eight @@ -190,12 +191,6 @@ async def async_setup(hass, config): DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA ) - async def stop_eight(event): - """Handle stopping eight api session.""" - await eight.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight) - return True diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 803b20383b6..d8a763c2e54 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Eight Sleep binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + BinarySensorEntity, +) from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity @@ -34,13 +37,15 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) self._usrobj = self._eight.users[self._userid] + self._attr_name = f"{name} {self._mapped_name}" + self._attr_device_class = DEVICE_CLASS_OCCUPANCY + _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", self._sensor, @@ -48,11 +53,6 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): self._userid, ) - @property - def name(self): - """Return the name of the sensor, if any.""" - return self._name - @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index d0f86d5a5e4..fb3762cf738 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.5"], + "requirements": ["pyeight==0.1.8"], "codeowners": ["@mezz64"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 87486dceef3..05cea4ea541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.eight_sleep -pyeight==0.1.5 +pyeight==0.1.8 # homeassistant.components.emby pyemby==1.7 From e4a7347e7d59f65253641a7c635d16ab417e4f50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Jul 2021 10:24:43 -0500 Subject: [PATCH 705/750] Import track_new_devices and scan_interval from yaml for nmap_tracker (#52409) * Import track_new_devices and scan_interval from yaml for nmap_tracker * Import track_new_devices and scan_interval from yaml for nmap_tracker * Import track_new_devices and scan_interval from yaml for nmap_tracker * tests * translate * tweak * adjust * save indent * pylint * There are two CONF_SCAN_INTERVAL constants * adjust name -- there are TWO CONF_SCAN_INTERVAL constants * remove CONF_SCAN_INTERVAL/CONF_TRACK_NEW from user flow * assert it does not appear in the user step --- .../components/nmap_tracker/__init__.py | 65 ++++++++++++------- .../components/nmap_tracker/config_flow.py | 55 +++++++++++----- .../components/nmap_tracker/const.py | 2 + .../components/nmap_tracker/device_tracker.py | 37 +++++++++-- .../components/nmap_tracker/strings.json | 6 +- .../nmap_tracker/translations/en.json | 4 +- .../nmap_tracker/test_config_flow.py | 25 +++++++ 7 files changed, 146 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 381813a3b49..399121e4e00 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -12,6 +12,10 @@ from getmac import get_mac_address from mac_vendor_lookup import AsyncMacLookup from nmap import PortScanner, PortScannerError +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant, callback @@ -25,6 +29,7 @@ import homeassistant.util.dt as dt_util from .const import ( CONF_HOME_INTERVAL, CONF_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, DOMAIN, NMAP_TRACKED_DEVICES, PLATFORMS, @@ -146,7 +151,10 @@ class NmapDeviceScanner: self._hosts = None self._options = None self._exclude = None + self._scan_interval = None + self._track_new_devices = None + self._known_mac_addresses = {} self._finished_first_scan = False self._last_results = [] self._mac_vendor_lookup = None @@ -154,6 +162,10 @@ class NmapDeviceScanner: async def async_setup(self): """Set up the tracker.""" config = self._entry.options + self._track_new_devices = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES) + self._scan_interval = timedelta( + seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) + ) self._hosts = cv.ensure_list_csv(config[CONF_HOSTS]) self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE]) self._options = config[CONF_OPTIONS] @@ -170,6 +182,12 @@ class NmapDeviceScanner: EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner ) ) + registry = er.async_get(self._hass) + self._known_mac_addresses = { + entry.unique_id: entry.original_name + for entry in registry.entities.values() + if entry.config_entry_id == self._entry_id + } @property def signal_device_new(self) -> str: @@ -199,7 +217,7 @@ class NmapDeviceScanner: async_track_time_interval( self._hass, self._async_scan_devices, - timedelta(seconds=TRACKER_SCAN_INTERVAL), + self._scan_interval, ) ) self._mac_vendor_lookup = AsyncMacLookup() @@ -258,26 +276,22 @@ class NmapDeviceScanner: # After all config entries have finished their first # scan we mark devices that were not found as not_home # from unavailable - registry = er.async_get(self._hass) now = dt_util.now() - for entry in registry.entities.values(): - if entry.config_entry_id != self._entry_id: + for mac_address, original_name in self._known_mac_addresses.items(): + if mac_address in self.devices.tracked: continue - if entry.unique_id not in self.devices.tracked: - self.devices.config_entry_owner[entry.unique_id] = self._entry_id - self.devices.tracked[entry.unique_id] = NmapDevice( - entry.unique_id, - None, - entry.original_name, - None, - self._async_get_vendor(entry.unique_id), - "Device not found in initial scan", - now, - 1, - ) - async_dispatcher_send( - self._hass, self.signal_device_missing, entry.unique_id - ) + self.devices.config_entry_owner[mac_address] = self._entry_id + self.devices.tracked[mac_address] = NmapDevice( + mac_address, + None, + original_name, + None, + self._async_get_vendor(mac_address), + "Device not found in initial scan", + now, + 1, + ) + async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) def _run_nmap_scan(self): """Run nmap and return the result.""" @@ -344,21 +358,28 @@ class NmapDeviceScanner: _LOGGER.info("No MAC address found for %s", ipv4) continue - hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - formatted_mac = format_mac(mac) + new = formatted_mac not in devices.tracked + if ( + new + and not self._track_new_devices + and formatted_mac not in devices.tracked + and formatted_mac not in self._known_mac_addresses + ): + continue + if ( devices.config_entry_owner.setdefault(formatted_mac, entry_id) != entry_id ): continue + hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) name = human_readable_name(hostname, vendor, mac) device = NmapDevice( formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 ) - new = formatted_mac not in devices.tracked devices.tracked[formatted_mac] = device devices.ipv4_last_mac[ipv4] = formatted_mac diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 942689ad575..68e61745b63 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -8,13 +8,24 @@ import ifaddr import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) DEFAULT_NETWORK_PREFIX = 24 @@ -92,23 +103,35 @@ def normalize_input(user_input): return errors -async def _async_build_schema_with_user_input(hass, user_input): +async def _async_build_schema_with_user_input(hass, user_input, include_options): hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) exclude = user_input.get( CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) ) - return vol.Schema( - { - vol.Required(CONF_HOSTS, default=hosts): str, - vol.Required( - CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) - ): int, - vol.Optional(CONF_EXCLUDE, default=exclude): str, - vol.Optional( - CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) - ): str, - } - ) + schema = { + vol.Required(CONF_HOSTS, default=hosts): str, + vol.Required( + CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) + ): int, + vol.Optional(CONF_EXCLUDE, default=exclude): str, + vol.Optional( + CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) + ): str, + } + if include_options: + schema.update( + { + vol.Optional( + CONF_TRACK_NEW, + default=user_input.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES), + ): bool, + vol.Optional( + CONF_SCAN_INTERVAL, + default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), + ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), + } + ) + return vol.Schema(schema) class OptionsFlowHandler(config_entries.OptionsFlow): @@ -133,7 +156,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form( step_id="init", data_schema=await _async_build_schema_with_user_input( - self.hass, self.options + self.hass, self.options, True ), errors=errors, ) @@ -170,7 +193,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=await _async_build_schema_with_user_input( - self.hass, self.options + self.hass, self.options, False ), errors=errors, ) diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index e71c2d58bbb..88118a81811 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -12,3 +12,5 @@ CONF_OPTIONS = "scan_options" DEFAULT_OPTIONS = "-F --host-timeout 5s" TRACKER_SCAN_INTERVAL = 120 + +DEFAULT_TRACK_NEW_DEVICES = True diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 24e0d3d8e26..350e75adf48 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -11,6 +11,11 @@ from homeassistant.components.device_tracker import ( SOURCE_TYPE_ROUTER, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import ( + CONF_NEW_DEVICE_DEFAULTS, + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback @@ -19,7 +24,14 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import NmapDeviceScanner, short_hostname, signal_device_update -from .const import CONF_HOME_INTERVAL, CONF_OPTIONS, DEFAULT_OPTIONS, DOMAIN +from .const import ( + CONF_HOME_INTERVAL, + CONF_OPTIONS, + DEFAULT_OPTIONS, + DEFAULT_TRACK_NEW_DEVICES, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) @@ -37,16 +49,27 @@ async def async_get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" validated_config = config[DEVICE_TRACKER_DOMAIN] + if CONF_SCAN_INTERVAL in validated_config: + scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() + else: + scan_interval = TRACKER_SCAN_INTERVAL + + import_config = { + CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), + CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], + CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), + CONF_OPTIONS: validated_config[CONF_OPTIONS], + CONF_SCAN_INTERVAL: scan_interval, + CONF_TRACK_NEW: validated_config.get(CONF_NEW_DEVICE_DEFAULTS, {}).get( + CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES + ), + } + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data={ - CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), - CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], - CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), - CONF_OPTIONS: validated_config[CONF_OPTIONS], - }, + data=import_config, ) ) diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index a1e04b681cd..ecb470a6f0d 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -8,8 +8,10 @@ "hosts": "[%key:component::nmap_tracker::config::step::user::data::hosts%]", "home_interval": "[%key:component::nmap_tracker::config::step::user::data::home_interval%]", "exclude": "[%key:component::nmap_tracker::config::step::user::data::exclude%]", - "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]" - } + "scan_options": "[%key:component::nmap_tracker::config::step::user::data::scan_options%]", + "track_new_devices": "Track new devices", + "interval_seconds": "Scan interval" + } } }, "error": { diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index ed37a6a5410..6b83532a0e2 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -28,7 +28,9 @@ "exclude": "Network addresses (comma seperated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma seperated) to scan", - "scan_options": "Raw configurable scan options for Nmap" + "interval_seconds": "Scan interval", + "scan_options": "Raw configurable scan options for Nmap", + "track_new_devices": "Track new devices" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 1556dee58d9..c4e82936b88 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -4,6 +4,10 @@ from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, +) from homeassistant.components.nmap_tracker.const import ( CONF_HOME_INTERVAL, CONF_OPTIONS, @@ -28,6 +32,10 @@ async def test_form(hass: HomeAssistant, hosts: str) -> None: assert result["type"] == "form" assert result["errors"] == {} + schema_defaults = result["data_schema"]({}) + assert CONF_TRACK_NEW not in schema_defaults + assert CONF_SCAN_INTERVAL not in schema_defaults + with patch( "homeassistant.components.nmap_tracker.async_setup_entry", return_value=True, @@ -198,6 +206,15 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + CONF_EXCLUDE: "4.4.4.4", + CONF_HOME_INTERVAL: 3, + CONF_HOSTS: "192.168.1.0/24", + CONF_SCAN_INTERVAL: 120, + CONF_OPTIONS: "-F --host-timeout 5s", + CONF_TRACK_NEW: True, + } + with patch( "homeassistant.components.nmap_tracker.async_setup_entry", return_value=True, @@ -209,6 +226,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 5, CONF_OPTIONS: "-sn", CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", + CONF_SCAN_INTERVAL: 10, + CONF_TRACK_NEW: False, }, ) await hass.async_block_till_done() @@ -219,6 +238,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 5, CONF_OPTIONS: "-sn", CONF_EXCLUDE: "4.4.4.4,5.5.5.5", + CONF_SCAN_INTERVAL: 10, + CONF_TRACK_NEW: False, } assert len(mock_setup_entry.mock_calls) == 1 @@ -238,6 +259,8 @@ async def test_import(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 3, CONF_OPTIONS: DEFAULT_OPTIONS, CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + CONF_TRACK_NEW: False, }, ) await hass.async_block_till_done() @@ -250,6 +273,8 @@ async def test_import(hass: HomeAssistant) -> None: CONF_HOME_INTERVAL: 3, CONF_OPTIONS: DEFAULT_OPTIONS, CONF_EXCLUDE: "4.4.4.4,6.4.3.2", + CONF_SCAN_INTERVAL: 2000, + CONF_TRACK_NEW: False, } assert len(mock_setup_entry.mock_calls) == 1 From 94638d316f571e220dcfe0e67c7be744dd710d78 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 13:17:00 +0200 Subject: [PATCH 706/750] Drop statistic_id and source columns from statistics table (#52417) * Drop statistic_id and source columns from statistics table * Remove useless double drop of statistics table * Update homeassistant/components/recorder/models.py Co-authored-by: Franck Nijhof * black Co-authored-by: Franck Nijhof --- .../components/recorder/migration.py | 16 ++- homeassistant/components/recorder/models.py | 25 ++-- .../components/recorder/statistics.py | 133 ++++++++++-------- homeassistant/components/sensor/recorder.py | 6 +- 4 files changed, 107 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b219209b386..30a5162e947 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -11,7 +11,14 @@ from sqlalchemy.exc import ( ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges, Statistics +from .models import ( + SCHEMA_VERSION, + TABLE_STATES, + Base, + SchemaChanges, + Statistics, + StatisticsMeta, +) from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -453,10 +460,15 @@ def _apply_update(engine, session, new_version, old_version): connection, engine, TABLE_STATES, ["old_state_id"] ) elif new_version == 17: + # This dropped the statistics table, done again in version 18. + pass + elif new_version == 18: if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics table + # Recreate the statistics and statisticsmeta tables Statistics.__table__.drop(engine) Statistics.__table__.create(engine) + StatisticsMeta.__table__.drop(engine) + StatisticsMeta.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 5a96cbf4f0b..50052c1f722 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -36,7 +36,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 17 +SCHEMA_VERSION = 18 _LOGGER = logging.getLogger(__name__) @@ -224,8 +224,11 @@ class Statistics(Base): # type: ignore __tablename__ = TABLE_STATISTICS id = Column(Integer, primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - source = Column(String(32)) - statistic_id = Column(String(255)) + metadata_id = Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) start = Column(DATETIME_TYPE, index=True) mean = Column(Float()) min = Column(Float()) @@ -236,15 +239,14 @@ class Statistics(Base): # type: ignore __table_args__ = ( # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "statistic_id", "start"), + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), ) @staticmethod - def from_stats(source, statistic_id, start, stats): + def from_stats(metadata_id, start, stats): """Create object from a statistics.""" return Statistics( - source=source, - statistic_id=statistic_id, + metadata_id=metadata_id, start=start, **stats, ) @@ -258,17 +260,22 @@ class StatisticsMeta(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_STATISTICS_META - statistic_id = Column(String(255), primary_key=True) + id = Column(Integer, primary_key=True) + statistic_id = Column(String(255), index=True) source = Column(String(32)) unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) @staticmethod - def from_meta(source, statistic_id, unit_of_measurement): + def from_meta(source, statistic_id, unit_of_measurement, has_mean, has_sum): """Create object from meta data.""" return StatisticsMeta( source=source, statistic_id=statistic_id, unit_of_measurement=unit_of_measurement, + has_mean=has_mean, + has_sum=has_sum, ) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0e01005c13a..e1dd0fb986a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from . import Recorder QUERY_STATISTICS = [ - Statistics.statistic_id, + Statistics.metadata_id, Statistics.start, Statistics.mean, Statistics.min, @@ -33,11 +33,8 @@ QUERY_STATISTICS = [ Statistics.sum, ] -QUERY_STATISTIC_IDS = [ - Statistics.statistic_id, -] - QUERY_STATISTIC_META = [ + StatisticsMeta.id, StatisticsMeta.statistic_id, StatisticsMeta.unit_of_measurement, ] @@ -76,16 +73,39 @@ def get_start_time() -> datetime.datetime: return start +def _get_metadata_ids(hass, session, statistic_ids): + """Resolve metadata_id for a list of statistic_ids.""" + baked_query = hass.data[STATISTICS_META_BAKERY]( + lambda session: session.query(*QUERY_STATISTIC_META) + ) + baked_query += lambda q: q.filter( + StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) + ) + result = execute(baked_query(session).params(statistic_ids=statistic_ids)) + + return [id for id, _, _ in result] + + +def _get_or_add_metadata_id(hass, session, statistic_id, metadata): + """Get metadata_id for a statistic_id, add if it doesn't exist.""" + metadata_id = _get_metadata_ids(hass, session, [statistic_id]) + if not metadata_id: + unit = metadata["unit_of_measurement"] + has_mean = metadata["has_mean"] + has_sum = metadata["has_sum"] + session.add( + StatisticsMeta.from_meta(DOMAIN, statistic_id, unit, has_mean, has_sum) + ) + metadata_id = _get_metadata_ids(hass, session, [statistic_id]) + return metadata_id[0] + + @retryable_database_job("statistics") def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: """Compile statistics.""" start = dt_util.as_utc(start) end = start + timedelta(hours=1) - _LOGGER.debug( - "Compiling statistics for %s-%s", - start, - end, - ) + _LOGGER.debug("Compiling statistics for %s-%s", start, end) platform_stats = [] for domain, platform in instance.hass.data[DOMAIN].items(): if not hasattr(platform, "compile_statistics"): @@ -98,29 +118,22 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: with session_scope(session=instance.get_session()) as session: # type: ignore for stats in platform_stats: for entity_id, stat in stats.items(): - session.add( - Statistics.from_stats(DOMAIN, entity_id, start, stat["stat"]) + metadata_id = _get_or_add_metadata_id( + instance.hass, session, entity_id, stat["meta"] ) - exists = session.query( - session.query(StatisticsMeta) - .filter_by(statistic_id=entity_id) - .exists() - ).scalar() - if not exists: - unit = stat["meta"]["unit_of_measurement"] - session.add(StatisticsMeta.from_meta(DOMAIN, entity_id, unit)) + session.add(Statistics.from_stats(metadata_id, start, stat["stat"])) return True -def _get_meta_data(hass, session, statistic_ids): +def _get_meta_data(hass, session, statistic_ids, statistic_type): """Fetch meta data.""" - def _meta(metas, wanted_statistic_id): - meta = {"statistic_id": wanted_statistic_id, "unit_of_measurement": None} - for statistic_id, unit in metas: - if statistic_id == wanted_statistic_id: - meta["unit_of_measurement"] = unit + def _meta(metas, wanted_metadata_id): + meta = None + for metadata_id, statistic_id, unit in metas: + if metadata_id == wanted_metadata_id: + meta = {"unit_of_measurement": unit, "statistic_id": statistic_id} return meta baked_query = hass.data[STATISTICS_META_BAKERY]( @@ -130,13 +143,14 @@ def _get_meta_data(hass, session, statistic_ids): baked_query += lambda q: q.filter( StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) ) - + if statistic_type == "mean": + baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False)) + if statistic_type == "sum": + baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False)) result = execute(baked_query(session).params(statistic_ids=statistic_ids)) - if statistic_ids is None: - statistic_ids = [statistic_id[0] for statistic_id in result] - - return {id: _meta(result, id) for id in statistic_ids} + metadata_ids = [metadata[0] for metadata in result] + return {id: _meta(result, id) for id in metadata_ids} def _configured_unit(unit: str, units) -> str: @@ -152,24 +166,11 @@ def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: - baked_query = hass.data[STATISTICS_BAKERY]( - lambda session: session.query(*QUERY_STATISTIC_IDS).distinct() - ) + meta_data = _get_meta_data(hass, session, None, statistic_type) - if statistic_type == "mean": - baked_query += lambda q: q.filter(Statistics.mean.isnot(None)) - if statistic_type == "sum": - baked_query += lambda q: q.filter(Statistics.sum.isnot(None)) - - baked_query += lambda q: q.order_by(Statistics.statistic_id) - - result = execute(baked_query(session)) - - statistic_ids = [statistic_id[0] for statistic_id in result] - meta_data = _get_meta_data(hass, session, statistic_ids) - for item in meta_data.values(): - unit = _configured_unit(item["unit_of_measurement"], units) - item["unit_of_measurement"] = unit + for meta in meta_data.values(): + unit = _configured_unit(meta["unit_of_measurement"], units) + meta["unit_of_measurement"] = unit return list(meta_data.values()) @@ -186,20 +187,24 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None if end_time is not None: baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time")) + metadata_ids = None if statistic_ids is not None: baked_query += lambda q: q.filter( - Statistics.statistic_id.in_(bindparam("statistic_ids")) + Statistics.metadata_id.in_(bindparam("metadata_ids")) ) statistic_ids = [statistic_id.lower() for statistic_id in statistic_ids] + metadata_ids = _get_metadata_ids(hass, session, statistic_ids) + if not metadata_ids: + return {} - baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start) + baked_query += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) stats = execute( baked_query(session).params( - start_time=start_time, end_time=end_time, statistic_ids=statistic_ids + start_time=start_time, end_time=end_time, metadata_ids=metadata_ids ) ) - meta_data = _get_meta_data(hass, session, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids, None) return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) @@ -210,23 +215,28 @@ def get_last_statistics(hass, number_of_stats, statistic_id=None): lambda session: session.query(*QUERY_STATISTICS) ) + metadata_id = None if statistic_id is not None: - baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) + metadata_ids = _get_metadata_ids(hass, session, [statistic_id]) + if not metadata_ids: + return {} + metadata_id = metadata_ids[0] baked_query += lambda q: q.order_by( - Statistics.statistic_id, Statistics.start.desc() + Statistics.metadata_id, Statistics.start.desc() ) baked_query += lambda q: q.limit(bindparam("number_of_stats")) stats = execute( baked_query(session).params( - number_of_stats=number_of_stats, statistic_id=statistic_id + number_of_stats=number_of_stats, metadata_id=metadata_id ) ) statistic_ids = [statistic_id] if statistic_id is not None else None - meta_data = _get_meta_data(hass, session, statistic_ids) + meta_data = _get_meta_data(hass, session, statistic_ids, None) return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) @@ -249,13 +259,14 @@ def _sorted_statistics_to_dict( _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat # Append all statistic entries, and do unit conversion - for ent_id, group in groupby(stats, lambda state: state.statistic_id): - unit = meta_data[ent_id]["unit_of_measurement"] + for meta_id, group in groupby(stats, lambda state: state.metadata_id): + unit = meta_data[meta_id]["unit_of_measurement"] + statistic_id = meta_data[meta_id]["statistic_id"] convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) - ent_results = result[ent_id] + ent_results = result[meta_id] ent_results.extend( { - "statistic_id": db_state.statistic_id, + "statistic_id": statistic_id, "start": _process_timestamp_to_utc_isoformat(db_state.start), "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), @@ -268,4 +279,4 @@ def _sorted_statistics_to_dict( ) # Filter out the empty lists if some states had 0 results. - return {key: val for key, val in result.items() if val} + return {meta_data[key]["statistic_id"]: val for key, val in result.items() if val} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index d9200bdf797..bbd49814076 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -224,7 +224,11 @@ def compile_statistics( result[entity_id] = {} # Set meta data - result[entity_id]["meta"] = {"unit_of_measurement": unit} + result[entity_id]["meta"] = { + "unit_of_measurement": unit, + "has_mean": "mean" in wanted_statistics, + "has_sum": "sum" in wanted_statistics, + } # Make calculations stat: dict = {} From 66680e44e45d288979e38aef469994bfca4ee8e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 14:10:32 +0200 Subject: [PATCH 707/750] Upgrade aioimaplib to 0.9.0 (#52422) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 5bb1efa0ca1..c1823459745 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.7.15"], + "requirements": ["aioimaplib==0.9.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 05cea4ea541..d8cd25d074c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aiohttp_cors==0.7.0 aiohue==2.5.1 # homeassistant.components.imap -aioimaplib==0.7.15 +aioimaplib==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From d04b0978dfd555b5abdc948576c1c2b66eabb0f1 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 2 Jul 2021 15:09:36 +0200 Subject: [PATCH 708/750] Fix typo in forecast_solar strings (#52430) --- homeassistant/components/forecast_solar/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index eb98fc79297..e1ae451a04f 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -17,7 +17,7 @@ "options": { "step": { "init": { - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation is a field is unclear.", + "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear.", "data": { "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", From 729f3dc6b8f0056eabab93c83f3995a601c772cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 15:40:54 +0200 Subject: [PATCH 709/750] Avoid duplicated database queries when fetching statistics (#52433) --- .../components/recorder/statistics.py | 51 +++++----- tests/components/recorder/test_statistics.py | 97 +++++++++++++++---- 2 files changed, 102 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index e1dd0fb986a..2ef49df7ded 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -126,7 +126,7 @@ def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: return True -def _get_meta_data(hass, session, statistic_ids, statistic_type): +def _get_metadata(hass, session, statistic_ids, statistic_type): """Fetch meta data.""" def _meta(metas, wanted_metadata_id): @@ -166,18 +166,23 @@ def list_statistic_ids(hass, statistic_type=None): """Return statistic_ids and meta data.""" units = hass.config.units with session_scope(hass=hass) as session: - meta_data = _get_meta_data(hass, session, None, statistic_type) + metadata = _get_metadata(hass, session, None, statistic_type) - for meta in meta_data.values(): + for meta in metadata.values(): unit = _configured_unit(meta["unit_of_measurement"], units) meta["unit_of_measurement"] = unit - return list(meta_data.values()) + return list(metadata.values()) def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None): """Return states changes during UTC period start_time - end_time.""" + metadata = None with session_scope(hass=hass) as session: + metadata = _get_metadata(hass, session, statistic_ids, None) + if not metadata: + return {} + baked_query = hass.data[STATISTICS_BAKERY]( lambda session: session.query(*QUERY_STATISTICS) ) @@ -192,10 +197,7 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None baked_query += lambda q: q.filter( Statistics.metadata_id.in_(bindparam("metadata_ids")) ) - statistic_ids = [statistic_id.lower() for statistic_id in statistic_ids] - metadata_ids = _get_metadata_ids(hass, session, statistic_ids) - if not metadata_ids: - return {} + metadata_ids = list(metadata.keys()) baked_query += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) @@ -204,24 +206,23 @@ def statistics_during_period(hass, start_time, end_time=None, statistic_ids=None start_time=start_time, end_time=end_time, metadata_ids=metadata_ids ) ) - meta_data = _get_meta_data(hass, session, statistic_ids, None) - return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) -def get_last_statistics(hass, number_of_stats, statistic_id=None): - """Return the last number_of_stats statistics.""" +def get_last_statistics(hass, number_of_stats, statistic_id): + """Return the last number_of_stats statistics for a statistic_id.""" + statistic_ids = [statistic_id] with session_scope(hass=hass) as session: + metadata = _get_metadata(hass, session, statistic_ids, None) + if not metadata: + return {} + baked_query = hass.data[STATISTICS_BAKERY]( lambda session: session.query(*QUERY_STATISTICS) ) - metadata_id = None - if statistic_id is not None: - baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) - metadata_ids = _get_metadata_ids(hass, session, [statistic_id]) - if not metadata_ids: - return {} - metadata_id = metadata_ids[0] + baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) + metadata_id = next(iter(metadata.keys())) baked_query += lambda q: q.order_by( Statistics.metadata_id, Statistics.start.desc() @@ -235,16 +236,14 @@ def get_last_statistics(hass, number_of_stats, statistic_id=None): ) ) - statistic_ids = [statistic_id] if statistic_id is not None else None - meta_data = _get_meta_data(hass, session, statistic_ids, None) - return _sorted_statistics_to_dict(hass, stats, statistic_ids, meta_data) + return _sorted_statistics_to_dict(hass, stats, statistic_ids, metadata) def _sorted_statistics_to_dict( hass, stats, statistic_ids, - meta_data, + metadata, ): """Convert SQL results into JSON friendly data structure.""" result = defaultdict(list) @@ -260,8 +259,8 @@ def _sorted_statistics_to_dict( # Append all statistic entries, and do unit conversion for meta_id, group in groupby(stats, lambda state: state.metadata_id): - unit = meta_data[meta_id]["unit_of_measurement"] - statistic_id = meta_data[meta_id]["statistic_id"] + unit = metadata[meta_id]["unit_of_measurement"] + statistic_id = metadata[meta_id]["statistic_id"] convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) ent_results = result[meta_id] ent_results.extend( @@ -279,4 +278,4 @@ def _sorted_statistics_to_dict( ) # Filter out the empty lists if some states had 0 results. - return {meta_data[key]["statistic_id"]: val for key, val in result.items() if val} + return {metadata[key]["statistic_id"]: val for key, val in result.items() if val} diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 104617aee2c..32eaaaab842 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -8,7 +8,10 @@ from pytest import approx from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat -from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.components.recorder.statistics import ( + get_last_statistics, + statistics_during_period, +) from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -25,24 +28,69 @@ def test_compile_hourly_statistics(hass_recorder): hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(period="hourly", start=zero) - wait_recording_done(hass) for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): stats = statistics_during_period(hass, zero, **kwargs) - assert stats == { - "sensor.test1": [ - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero), - "mean": approx(14.915254237288135), - "min": approx(10.0), - "max": approx(20.0), - "last_reset": None, - "state": None, - "sum": None, - } - ] - } + assert stats == {} + stats = get_last_statistics(hass, 0, "sensor.test1") + assert stats == {} + + recorder.do_adhoc_statistics(period="hourly", start=zero) + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + expected_1 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_2 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(four), + "mean": approx(20.0), + "min": approx(20.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_stats1 = [ + {**expected_1, "statistic_id": "sensor.test1"}, + {**expected_2, "statistic_id": "sensor.test1"}, + ] + expected_stats2 = [ + {**expected_1, "statistic_id": "sensor.test2"}, + {**expected_2, "statistic_id": "sensor.test2"}, + ] + + # Test statistics_during_period + stats = statistics_during_period(hass, zero) + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test2"]) + assert stats == {"sensor.test2": expected_stats2} + + stats = statistics_during_period(hass, zero, statistic_ids=["sensor.test3"]) + assert stats == {} + + # Test get_last_statistics + stats = get_last_statistics(hass, 0, "sensor.test1") + assert stats == {} + + stats = get_last_statistics(hass, 1, "sensor.test1") + assert stats == {"sensor.test1": [{**expected_2, "statistic_id": "sensor.test1"}]} + + stats = get_last_statistics(hass, 2, "sensor.test1") + assert stats == {"sensor.test1": expected_stats1[::-1]} + + stats = get_last_statistics(hass, 3, "sensor.test1") + assert stats == {"sensor.test1": expected_stats1[::-1]} + + stats = get_last_statistics(hass, 1, "sensor.test3") + assert stats == {} def record_states(hass): @@ -54,13 +102,19 @@ def record_states(hass): sns1 = "sensor.test1" sns2 = "sensor.test2" sns3 = "sensor.test3" + sns4 = "sensor.test4" sns1_attr = { "device_class": "temperature", "state_class": "measurement", "unit_of_measurement": TEMP_CELSIUS, } - sns2_attr = {"device_class": "temperature"} - sns3_attr = {} + sns2_attr = { + "device_class": "humidity", + "state_class": "measurement", + "unit_of_measurement": "%", + } + sns3_attr = {"device_class": "temperature"} + sns4_attr = {} def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -74,7 +128,7 @@ def record_states(hass): three = two + timedelta(minutes=30) four = three + timedelta(minutes=15) - states = {mp: [], sns1: [], sns2: [], sns3: []} + states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []} with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) @@ -85,15 +139,18 @@ def record_states(hass): states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "15", attributes=sns4_attr)) with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + states[sns4].append(set_state(sns4, "20", attributes=sns4_attr)) return zero, four, states From 730c8cbcc4ff415fcc45fed56676ee1bf8b440d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 2 Jul 2021 16:28:16 +0200 Subject: [PATCH 710/750] Correct recorder table arguments (#52436) --- homeassistant/components/recorder/models.py | 52 +++++++-------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 50052c1f722..c77d824c64f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -66,10 +66,12 @@ DATETIME_TYPE = DateTime(timezone=True).with_variant( class Events(Base): # type: ignore """Event history data.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) __tablename__ = TABLE_EVENTS event_id = Column(Integer, Identity(), primary_key=True) event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) @@ -81,12 +83,6 @@ class Events(Base): # type: ignore context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), - ) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( @@ -133,10 +129,12 @@ class Events(Base): # type: ignore class States(Base): # type: ignore """State change history.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) __tablename__ = TABLE_STATES state_id = Column(Integer, Identity(), primary_key=True) domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) @@ -153,12 +151,6 @@ class States(Base): # type: ignore event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), - ) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( @@ -217,10 +209,10 @@ class States(Base): # type: ignore class Statistics(Base): # type: ignore """Statistics.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) __tablename__ = TABLE_STATISTICS id = Column(Integer, primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) @@ -237,11 +229,6 @@ class Statistics(Base): # type: ignore state = Column(Float()) sum = Column(Float()) - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start"), - ) - @staticmethod def from_stats(metadata_id, start, stats): """Create object from a statistics.""" @@ -255,10 +242,6 @@ class Statistics(Base): # type: ignore class StatisticsMeta(Base): # type: ignore """Statistics meta data.""" - __table_args__ = { - "mysql_default_charset": "utf8mb4", - "mysql_collate": "utf8mb4_unicode_ci", - } __tablename__ = TABLE_STATISTICS_META id = Column(Integer, primary_key=True) statistic_id = Column(String(255), index=True) @@ -282,6 +265,7 @@ class StatisticsMeta(Base): # type: ignore class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) __tablename__ = TABLE_RECORDER_RUNS run_id = Column(Integer, Identity(), primary_key=True) start = Column(DateTime(timezone=True), default=dt_util.utcnow) @@ -289,8 +273,6 @@ class RecorderRuns(Base): # type: ignore closed_incorrect = Column(Boolean, default=False) created = Column(DateTime(timezone=True), default=dt_util.utcnow) - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - def __repr__(self) -> str: """Return string representation of instance for debugging.""" end = ( From 8b54d958f35367bf31aec168f3e641427b486ecc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 17:58:59 +0200 Subject: [PATCH 711/750] Bumped version to 2021.7.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 89fccd434ed..4e64162c756 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 4b3ce4763d80347b617b378a2f21ffa87e75d6ef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 20:56:51 +0200 Subject: [PATCH 712/750] Abort existing reauth flow on entry removal (#52407) --- homeassistant/config_entries.py | 13 +++++++++++++ tests/test_config_entries.py | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 49892937217..2bb8c4f3e29 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -819,6 +819,19 @@ class ConfigEntries: dev_reg.async_clear_config_entry(entry_id) ent_reg.async_clear_config_entry(entry_id) + # If the configuration entry is removed during reauth, it should + # abort any reauth flow that is active for the removed entry. + for progress_flow in self.hass.config_entries.flow.async_progress(): + context = progress_flow.get("context") + if ( + context + and context["source"] == SOURCE_REAUTH + and "entry_id" in context + and context["entry_id"] == entry_id + and "flow_id" in progress_flow + ): + self.hass.config_entries.flow.async_abort(progress_flow["flow_id"]) + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a # user is able to immediately start configuring it. We do this by starting a new flow with # the 'unignore' step. If the integration doesn't implement async_step_unignore then diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 615b97fb990..7bcc83048a4 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -337,6 +337,30 @@ async def test_remove_entry(hass, manager): assert not entity_entry_list +async def test_remove_entry_cancels_reauth(hass, manager): + """Tests that removing a config entry, also aborts existing reauth flows.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryAuthFailed()) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + entry.add_to_hass(hass) + await entry.async_setup(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + + await manager.async_remove(entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + async def test_remove_entry_handles_callback_error(hass, manager): """Test that exceptions in the remove callback are handled.""" mock_setup_entry = AsyncMock(return_value=True) From 77c643946b45d596112e6ac59e1f8a64053675c5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 2 Jul 2021 19:21:05 +0200 Subject: [PATCH 713/750] Fix Fritz call deflection list (#52443) --- homeassistant/components/fritz/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 09bae36a29c..d9690b64069 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -110,7 +110,10 @@ def get_deflections( if not deflection_list: return [] - return [xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"]] + items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] + if not isinstance(items, list): + return [items] + return items def deflection_entities_list( From 4b077b5a391070e9849a9f6433df1ab6b251fd3e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 20:55:40 +0200 Subject: [PATCH 714/750] Fix Statistics recorder migration order (#52449) --- homeassistant/components/recorder/migration.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 30a5162e947..e931abbd2d3 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -347,7 +347,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): ) -def _apply_update(engine, session, new_version, old_version): +def _apply_update(engine, session, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" connection = session.connection() if new_version == 1: @@ -463,12 +463,15 @@ def _apply_update(engine, session, new_version, old_version): # This dropped the statistics table, done again in version 18. pass elif new_version == 18: - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics and statisticsmeta tables - Statistics.__table__.drop(engine) - Statistics.__table__.create(engine) + # Recreate the statisticsmeta tables + if sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__): StatisticsMeta.__table__.drop(engine) - StatisticsMeta.__table__.create(engine) + StatisticsMeta.__table__.create(engine) + + # Recreate the statistics table + if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + Statistics.__table__.drop(engine) + Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") From 95132cc425bcb202b1a8399c663f6db5edb04e08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 2 Jul 2021 21:07:17 +0200 Subject: [PATCH 715/750] Bumped version to 2021.7.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4e64162c756..d35c44348c5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From d2cef65b6392fe08ca8ae0670b28bee502927618 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 5 Jul 2021 09:23:02 +0200 Subject: [PATCH 716/750] Bump gios library to version 1.0.2 (#52527) --- homeassistant/components/gios/air_quality.py | 4 ++-- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index e74cec8e151..00c4a526c46 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -80,7 +80,7 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity): @property def air_quality_index(self) -> str | None: """Return the air quality index.""" - return cast(Optional[str], self.coordinator.data.get(API_AQI, {}).get("value")) + return cast(Optional[str], self.coordinator.data.get(API_AQI).get("value")) @property def particulate_matter_2_5(self) -> float | None: @@ -141,7 +141,7 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity): if sensor in self.coordinator.data: self._attrs[f"{SENSOR_MAP[sensor]}_index"] = self.coordinator.data[ sensor - ]["index"] + ].get("index") self._attrs[ATTR_STATION] = self.coordinator.gios.station_name return self._attrs diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3dfb2a168db..f13da0e3f33 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==1.0.1"], + "requirements": ["gios==1.0.2"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index d8cd25d074c..1c5d7449ced 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,7 +678,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.1 +gios==1.0.2 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f79b8764e58..30beb930b9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==1.0.1 +gios==1.0.2 # homeassistant.components.glances glances_api==0.2.0 From ebc3e1f6583d68cdc24efb77cf0080f665fda1d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Jul 2021 13:34:40 +0200 Subject: [PATCH 717/750] Fix Statistics recorder migration path by dropping in pairs (#52453) --- .../components/recorder/migration.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index e931abbd2d3..248c4597b9f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -463,14 +463,19 @@ def _apply_update(engine, session, new_version, old_version): # noqa: C901 # This dropped the statistics table, done again in version 18. pass elif new_version == 18: - # Recreate the statisticsmeta tables - if sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__): - StatisticsMeta.__table__.drop(engine) - StatisticsMeta.__table__.create(engine) + # Recreate the statistics and statistics meta tables. + # + # Order matters! Statistics has a relation with StatisticsMeta, + # so statistics need to be deleted before meta (or in pair depending + # on the SQL backend); and meta needs to be created before statistics. + if sqlalchemy.inspect(engine).has_table( + StatisticsMeta.__tablename__ + ) or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + Base.metadata.drop_all( + bind=engine, tables=[Statistics.__table__, StatisticsMeta.__table__] + ) - # Recreate the statistics table - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - Statistics.__table__.drop(engine) + StatisticsMeta.__table__.create(engine) Statistics.__table__.create(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") From 070991c160ab38b3b15ec49873e3df669bf602fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 Jul 2021 10:01:41 -0500 Subject: [PATCH 718/750] Bump aiohomekit to 0.4.1 (#52472) - Fixes mdns queries being sent with the original case received on the wire Some responders were case sensitive and needed the original case sent - Reduces mdns traffic --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 155f3a4f5f6..39144d6c521 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.0"], + "requirements": ["aiohomekit==0.4.1"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 1c5d7449ced..5b9d2556af7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.0 +aiohomekit==0.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30beb930b9c..9d67b86a9a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.0 +aiohomekit==0.4.1 # homeassistant.components.emulated_hue # homeassistant.components.http From afb187942a145e76ddb866482847e6d65d6b1034 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 5 Jul 2021 04:38:31 -0500 Subject: [PATCH 719/750] Revert "Force SimpliSafe to reauthenticate with a password (#51528)" (#52484) This reverts commit 549f779b0679b004f67aca996b107414355a6e36. --- .../components/simplisafe/__init__.py | 73 ++++++++++++------- .../components/simplisafe/config_flow.py | 13 ++-- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- .../components/simplisafe/test_config_flow.py | 20 ++--- 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b7c2a08f093..983629743e7 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -6,7 +6,7 @@ from simplipy import API from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -107,6 +107,14 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) +@callback +def _async_save_refresh_token(hass, config_entry, token): + """Save a refresh token to the config entry.""" + hass.config_entries.async_update_entry( + config_entry, data={**config_entry.data, CONF_TOKEN: token} + ) + + async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -134,9 +142,6 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] - if CONF_PASSWORD not in config_entry.data: - raise ConfigEntryAuthFailed("Config schema change requires re-authentication") - entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -158,24 +163,20 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) - async def async_get_api(): - """Define a helper to get an authenticated SimpliSafe API object.""" - return await API.login_via_credentials( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], - client_id=client_id, - session=websession, - ) - try: - api = await async_get_api() - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed from err + api = await API.login_via_token( + config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + ) + except InvalidCredentialsError: + LOGGER.error("Invalid credentials provided") + return False except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - simplisafe = SimpliSafe(hass, config_entry, api, async_get_api) + _async_save_refresh_token(hass, config_entry, api.refresh_token) + + simplisafe = SimpliSafe(hass, api, config_entry) try: await simplisafe.async_init() @@ -302,10 +303,10 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, config_entry, api, async_get_api): + def __init__(self, hass, api, config_entry): """Initialize.""" self._api = api - self._async_get_api = async_get_api + self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -382,17 +383,23 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - try: - self._api = await self._async_get_api() - return - except InvalidCredentialsError as err: + if self._emergency_refresh_token_used: raise ConfigEntryAuthFailed( - "Unable to re-authenticate with SimpliSafe" - ) from err + "Update failed with stored refresh token" + ) + + LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + self._emergency_refresh_token_used = True + + try: + await self._api.refresh_access_token( + self.config_entry.data[CONF_TOKEN] + ) + return except SimplipyError as err: - raise UpdateFailed( - f"SimpliSafe error while updating: {err}" - ) from err + raise UpdateFailed( # pylint: disable=raise-missing-from + f"Error while using stored refresh token: {err}" + ) if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -403,6 +410,16 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") + if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: + _async_save_refresh_token( + self._hass, self.config_entry, self._api.refresh_token + ) + + # If we've reached this point using an emergency refresh token, we're in the + # clear and we can discard it: + if self._emergency_refresh_token_used: + self._emergency_refresh_token_used = False + class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0faa07221aa..ba51356f770 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) @@ -89,9 +89,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -101,7 +98,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -111,7 +108,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 23f85495025..ad973261a0e 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 331eb65ca83..b9e274666bb 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index c2397e9f89e..a048e4b0745 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry @@ -33,11 +33,7 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -106,11 +102,7 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -128,8 +120,6 @@ async def test_step_reauth(hass): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) - ), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -161,7 +151,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", } @@ -207,7 +197,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", } From 36eec7ddbc4f290100596fcf5560d5671e3c83b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Jul 2021 11:40:33 -0500 Subject: [PATCH 720/750] Remove empty hosts and excludes from nmap configuration (#52489) --- homeassistant/components/nmap_tracker/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 399121e4e00..76a7e44f153 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -166,8 +166,10 @@ class NmapDeviceScanner: self._scan_interval = timedelta( seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) ) - self._hosts = cv.ensure_list_csv(config[CONF_HOSTS]) - self._exclude = cv.ensure_list_csv(config[CONF_EXCLUDE]) + hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) + self._hosts = [host for host in hosts_list if host != ""] + excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) + self._exclude = [exclude for exclude in excludes_list if exclude != ""] self._options = config[CONF_OPTIONS] self.home_interval = timedelta( minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) From 206437b10cab6840aa4b2aa2532015d69cebe8c7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 5 Jul 2021 11:45:50 +0200 Subject: [PATCH 721/750] Fix MODBUS connection type rtuovertcp does not connect (#52505) * Correct host -> framer. * Use function pointer --- homeassistant/components/modbus/modbus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 35572baff43..2e5892dbf1d 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -5,6 +5,7 @@ import logging from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException +from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( CONF_DELAY, @@ -224,7 +225,7 @@ class ModbusHub: # network configuration self._pb_params["host"] = client_config[CONF_HOST] if self._config_type == CONF_RTUOVERTCP: - self._pb_params["host"] = "ModbusRtuFramer" + self._pb_params["framer"] = ModbusRtuFramer Defaults.Timeout = client_config[CONF_TIMEOUT] From e140cd9b6ab5d16dbc6b5cf05309e268a5891259 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Jul 2021 09:16:42 -0500 Subject: [PATCH 722/750] Bump HAP-python to 3.5.1 (#52508) - Fixes additional cases of invalid mdns hostnames --- 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 d2c2f094a0f..39c40e03614 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.5.0", + "HAP-python==3.5.1", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/requirements_all.txt b/requirements_all.txt index 5b9d2556af7..0b9e2b92f2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.5.0 +HAP-python==3.5.1 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d67b86a9a4..055ec38f2a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.5.0 +HAP-python==3.5.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From d5b419eeda50bc14c2bddaf26db618e395375b50 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Jul 2021 13:28:01 +0200 Subject: [PATCH 723/750] Remove problematic/redudant db migration happning schema 15 (#52541) --- homeassistant/components/recorder/migration.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 248c4597b9f..2ed676bfdb9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -347,7 +347,7 @@ def _drop_foreign_key_constraints(connection, engine, table, columns): ) -def _apply_update(engine, session, new_version, old_version): # noqa: C901 +def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" connection = session.connection() if new_version == 1: @@ -451,10 +451,8 @@ def _apply_update(engine, session, new_version, old_version): # noqa: C901 elif new_version == 14: _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) elif new_version == 15: - if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - # Recreate the statistics table - Statistics.__table__.drop(engine) - Statistics.__table__.create(engine) + # This dropped the statistics table, done again in version 18. + pass elif new_version == 16: _drop_foreign_key_constraints( connection, engine, TABLE_STATES, ["old_state_id"] From 9368f75cec330ce4bb61086de0167a10a5a2baa7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 5 Jul 2021 13:41:25 +0200 Subject: [PATCH 724/750] Bumped version to 2021.7.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d35c44348c5..5a58a3e2789 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 0cd097cd1240193fa66f012199f747bca5747a42 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 5 Jul 2021 15:22:41 +0100 Subject: [PATCH 725/750] Update list of supported Coinbase wallet currencies (#52545) --- homeassistant/components/coinbase/const.py | 201 ++++++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 1f86c8026ec..035706c46ce 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -19,50 +19,247 @@ API_ACCOUNTS_DATA = "data" API_RATES = "rates" WALLETS = { + "1INCH": "1INCH", "AAVE": "AAVE", + "ADA": "ADA", + "AED": "AED", + "AFN": "AFN", "ALGO": "ALGO", + "ALL": "ALL", + "AMD": "AMD", + "AMP": "AMP", + "ANG": "ANG", + "ANKR": "ANKR", + "AOA": "AOA", + "ARS": "ARS", "ATOM": "ATOM", + "AUD": "AUD", + "AWG": "AWG", + "AZN": "AZN", "BAL": "BAL", + "BAM": "BAM", "BAND": "BAND", "BAT": "BAT", + "BBD": "BBD", "BCH": "BCH", + "BDT": "BDT", + "BGN": "BGN", + "BHD": "BHD", + "BIF": "BIF", + "BMD": "BMD", + "BND": "BND", "BNT": "BNT", + "BOB": "BOB", + "BOND": "BOND", + "BRL": "BRL", + "BSD": "BSD", "BSV": "BSV", "BTC": "BTC", - "CGLD": "CLGD", - "CVC": "CVC", + "BTN": "BTN", + "BWP": "BWP", + "BYN": "BYN", + "BYR": "BYR", + "BZD": "BZD", + "CAD": "CAD", + "CDF": "CDF", + "CGLD": "CGLD", + "CHF": "CHF", + "CHZ": "CHZ", + "CLF": "CLF", + "CLP": "CLP", + "CNH": "CNH", + "CNY": "CNY", "COMP": "COMP", + "COP": "COP", + "CRC": "CRC", + "CRV": "CRV", + "CTSI": "CTSI", + "CUC": "CUC", + "CVC": "CVC", + "CVE": "CVE", + "CZK": "CZK", "DAI": "DAI", + "DASH": "DASH", + "DJF": "DJF", + "DKK": "DKK", "DNT": "DNT", + "DOGE": "DOGE", + "DOP": "DOP", + "DOT": "DOT", + "DZD": "DZD", + "EGP": "EGP", + "ENJ": "ENJ", "EOS": "EOS", + "ERN": "ERN", + "ETB": "ETB", "ETC": "ETC", "ETH": "ETH", + "ETH2": "ETH2", "EUR": "EUR", "FIL": "FIL", + "FJD": "FJD", + "FKP": "FKP", + "FORTH": "FORTH", "GBP": "GBP", + "GBX": "GBX", + "GEL": "GEL", + "GGP": "GGP", + "GHS": "GHS", + "GIP": "GIP", + "GMD": "GMD", + "GNF": "GNF", "GRT": "GRT", + "GTC": "GTC", + "GTQ": "GTQ", + "GYD": "GYD", + "HKD": "HKD", + "HNL": "HNL", + "HRK": "HRK", + "HTG": "HTG", + "HUF": "HUF", + "ICP": "ICP", + "IDR": "IDR", + "ILS": "ILS", + "IMP": "IMP", + "INR": "INR", + "IQD": "IQD", + "ISK": "ISK", + "JEP": "JEP", + "JMD": "JMD", + "JOD": "JOD", + "JPY": "JPY", + "KEEP": "KEEP", + "KES": "KES", + "KGS": "KGS", + "KHR": "KHR", + "KMF": "KMF", "KNC": "KNC", + "KRW": "KRW", + "KWD": "KWD", + "KYD": "KYD", + "KZT": "KZT", + "LAK": "LAK", + "LBP": "LBP", "LINK": "LINK", + "LKR": "LKR", + "LPT": "LPT", "LRC": "LRC", + "LRD": "LRD", + "LSL": "LSL", "LTC": "LTC", + "LYD": "LYD", + "MAD": "MAD", "MANA": "MANA", + "MATIC": "MATIC", + "MDL": "MDL", + "MGA": "MGA", + "MIR": "MIR", + "MKD": "MKD", "MKR": "MKR", + "MLN": "MLN", + "MMK": "MMK", + "MNT": "MNT", + "MOP": "MOP", + "MRO": "MRO", + "MTL": "MTL", + "MUR": "MUR", + "MVR": "MVR", + "MWK": "MWK", + "MXN": "MXN", + "MYR": "MYR", + "MZN": "MZN", + "NAD": "NAD", + "NGN": "NGN", + "NIO": "NIO", + "NKN": "NKN", "NMR": "NMR", + "NOK": "NOK", + "NPR": "NPR", "NU": "NU", + "NZD": "NZD", + "OGN": "OGN", "OMG": "OMG", + "OMR": "OMR", "OXT": "OXT", + "PAB": "PAB", + "PEN": "PEN", + "PGK": "PGK", + "PHP": "PHP", + "PKR": "PKR", + "PLN": "PLN", + "PYG": "PYG", + "QAR": "QAR", + "QNT": "QNT", + "REN": "REN", "REP": "REP", "REPV2": "REPV2", + "RLC": "RLC", + "RON": "RON", + "RSD": "RSD", + "RUB": "RUB", + "RWF": "RWF", + "SAR": "SAR", + "SBD": "SBD", + "SCR": "SCR", + "SEK": "SEK", + "SGD": "SGD", + "SHP": "SHP", + "SKL": "SKL", + "SLL": "SLL", "SNX": "SNX", + "SOL": "SOL", + "SOS": "SOS", + "SRD": "SRD", + "SSP": "SSP", + "STD": "STD", + "STORJ": "STORJ", + "SUSHI": "SUSHI", + "SVC": "SVC", + "SZL": "SZL", + "THB": "THB", + "TJS": "TJS", + "TMM": "TMM", + "TMT": "TMT", + "TND": "TND", + "TOP": "TOP", + "TRB": "TRB", + "TRY": "TRY", + "TTD": "TTD", + "TWD": "TWD", + "TZS": "TZS", + "UAH": "UAH", + "UGX": "UGX", "UMA": "UMA", "UNI": "UNI", + "USD": "USD", "USDC": "USDC", + "USDT": "USDT", + "UYU": "UYU", + "UZS": "UZS", + "VES": "VES", + "VND": "VND", + "VUV": "VUV", "WBTC": "WBTC", + "WST": "WST", + "XAF": "XAF", + "XAG": "XAG", + "XAU": "XAU", + "XCD": "XCD", + "XDR": "XDR", "XLM": "XLM", + "XOF": "XOF", + "XPD": "XPD", + "XPF": "XPF", + "XPT": "XPT", "XRP": "XRP", "XTZ": "XTZ", + "YER": "YER", "YFI": "YFI", + "ZAR": "ZAR", + "ZEC": "ZEC", + "ZMW": "ZMW", "ZRX": "ZRX", + "ZWL": "ZWL", } RATES = { From dfce89f2c70f8252e1c178d4643f623a9a0fc439 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 09:17:11 -0500 Subject: [PATCH 726/750] Bump zeroconf to 0.32.1 (#52547) - Changelog: https://github.com/jstasiak/python-zeroconf/compare/0.32.0...0.32.1 - Fixes #52384 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 0f7d18446ee..199275623dc 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.32.0"], + "requirements": ["zeroconf==0.32.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e166af6172..98ce2b67183 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.32.0 +zeroconf==0.32.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 0b9e2b92f2d..f34669d4b9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2428,7 +2428,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.32.0 +zeroconf==0.32.1 # homeassistant.components.zha zha-quirks==0.0.58 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 055ec38f2a1..f07189c2753 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ yeelight==0.6.3 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.32.0 +zeroconf==0.32.1 # homeassistant.components.zha zha-quirks==0.0.58 From a52b4b0f626d92b0a37bfeb979796457ab43dc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 5 Jul 2021 22:04:55 +0200 Subject: [PATCH 727/750] Bump pysma version to 0.6.2 (#52553) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 85f6de7cb7c..a48b9ba74ce 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.1"], + "requirements": ["pysma==0.6.2"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f34669d4b9b..a0a812f9e19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1756,7 +1756,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.1 +pysma==0.6.2 # homeassistant.components.smappee pysmappee==0.2.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f07189c2753..6c6e58d1f2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ pysiaalarm==3.0.0 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.1 +pysma==0.6.2 # homeassistant.components.smappee pysmappee==0.2.25 From 777cf116aab77407bdb4a7e2bf77ea0416029f16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 15:16:49 -0500 Subject: [PATCH 728/750] Update the ip/port in the homekit_controller config entry when it changes (#52554) --- .../homekit_controller/config_flow.py | 14 ++++- .../homekit_controller/test_config_flow.py | 57 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index ebb14e43378..e8357a4001d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -236,9 +236,20 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) config_num = None + # Set unique-id and error out if it's already configured + existing_entry = await self.async_set_unique_id(normalize_hkid(hkid)) + updated_ip_port = { + "AccessoryIP": discovery_info["host"], + "AccessoryPort": discovery_info["port"], + } + # If the device is already paired and known to us we should monitor c# # (config_num) for changes. If it changes, we check for new entities if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): + if existing_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data={**existing_entry.data, **updated_ip_port} + ) conn = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait @@ -262,8 +273,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_remove(existing.entry_id) # Set unique-id and error out if it's already configured - await self.async_set_unique_id(normalize_hkid(hkid)) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=updated_ip_port) self.context["hkid"] = hkid diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 99c6966e827..52685334500 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for homekit_controller config flow.""" from unittest import mock import unittest.mock -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import aiohomekit from aiohomekit.model import Accessories, Accessory @@ -11,6 +11,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.homekit_controller import config_flow +from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.helpers import device_registry from tests.common import MockConfigEntry, mock_device_registry @@ -383,11 +384,16 @@ async def test_discovery_invalid_config_entry(hass, controller): async def test_discovery_already_configured(hass, controller): """Already configured.""" - MockConfigEntry( + entry = MockConfigEntry( domain="homekit_controller", - data={"AccessoryPairingID": "00:00:00:00:00:00"}, + data={ + "AccessoryIP": "4.4.4.4", + "AccessoryPort": 66, + "AccessoryPairingID": "00:00:00:00:00:00", + }, unique_id="00:00:00:00:00:00", - ).add_to_hass(hass) + ) + entry.add_to_hass(hass) device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) @@ -403,6 +409,49 @@ async def test_discovery_already_configured(hass, controller): ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + assert entry.data["AccessoryIP"] == discovery_info["host"] + assert entry.data["AccessoryPort"] == discovery_info["port"] + + +async def test_discovery_already_configured_update_csharp(hass, controller): + """Already configured and csharp changes.""" + entry = MockConfigEntry( + domain="homekit_controller", + data={ + "AccessoryIP": "4.4.4.4", + "AccessoryPort": 66, + "AccessoryPairingID": "AA:BB:CC:DD:EE:FF", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + connection_mock = AsyncMock() + connection_mock.pairing.connect.reconnect_soon = AsyncMock() + connection_mock.async_refresh_entity_map = AsyncMock() + hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} + + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Set device as already paired + discovery_info["properties"]["sf"] = 0x00 + discovery_info["properties"]["c#"] = 99999 + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert entry.data["AccessoryIP"] == discovery_info["host"] + assert entry.data["AccessoryPort"] == discovery_info["port"] + assert connection_mock.async_refresh_entity_map.await_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) From 1c9053fef6a635a928e779dc6943d7e08ff0967a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 5 Jul 2021 14:00:32 -0400 Subject: [PATCH 729/750] Bump up zha dependencies (#52555) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index b366b73d6c8..68feabb18b4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -10,7 +10,7 @@ "zha-quirks==0.0.58", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", - "zigpy==0.35.0", + "zigpy==0.35.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.1" diff --git a/requirements_all.txt b/requirements_all.txt index a0a812f9e19..064e7b3bc7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2455,7 +2455,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.0 +zigpy==0.35.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c6e58d1f2f..990a0e24a8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.1 # homeassistant.components.zha -zigpy==0.35.0 +zigpy==0.35.1 # homeassistant.components.zwave_js zwave-js-server-python==0.27.0 From 701fa06584f70f5c9a18dfef11905c4dfeccdf7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Jul 2021 15:00:57 -0500 Subject: [PATCH 730/750] Bump aiohomekit to 0.4.2 (#52560) - Changelog: https://github.com/Jc2k/aiohomekit/compare/0.4.1...0.4.2 - Fixes: #52548 --- homeassistant/components/homekit_controller/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_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 39144d6c521..496d629d112 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.4.1"], + "requirements": ["aiohomekit==0.4.2"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 064e7b3bc7f..57c7e6ce466 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.1 +aiohomekit==0.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 990a0e24a8a..90a700867ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.4.1 +aiohomekit==0.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 979d37dc199e5640f7e306076abc2541ab183a72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Jul 2021 09:33:00 +0200 Subject: [PATCH 731/750] Fix unavailable entity capable of triggering non-numerical warning in Threshold sensor (#52563) --- .../components/threshold/binary_sensor.py | 5 ++++- .../threshold/test_binary_sensor.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 5bd6f77253b..1a53a599394 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -100,7 +101,9 @@ class ThresholdSensor(BinarySensorEntity): try: self.sensor_value = ( - None if new_state.state == STATE_UNKNOWN else float(new_state.state) + None + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + else float(new_state.state) ) except (ValueError, TypeError): self.sensor_value = None diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index af8c32a1549..b7c4a871068 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -1,6 +1,11 @@ """The test for the threshold sensor platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.setup import async_setup_component @@ -283,7 +288,7 @@ async def test_sensor_in_range_with_hysteresis(hass): assert state.state == "on" -async def test_sensor_in_range_unknown_state(hass): +async def test_sensor_in_range_unknown_state(hass, caplog): """Test if source is within the range.""" config = { "binary_sensor": { @@ -322,6 +327,16 @@ async def test_sensor_in_range_unknown_state(hass): assert state.attributes.get("position") == "unknown" assert state.state == "off" + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.threshold") + + assert state.attributes.get("position") == "unknown" + assert state.state == "off" + + assert "State is not numerical" not in caplog.text + async def test_sensor_lower_zero_threshold(hass): """Test if a lower threshold of zero is set.""" From 2220c8cd3f3ecb649cb1bd9d52a622a24df24782 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Jul 2021 02:41:23 -0400 Subject: [PATCH 732/750] Bump pyeight version to 0.1.9 (#52568) --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index fb3762cf738..1c3944a985e 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.8"], + "requirements": ["pyeight==0.1.9"], "codeowners": ["@mezz64"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 57c7e6ce466..c056c39409a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.eight_sleep -pyeight==0.1.8 +pyeight==0.1.9 # homeassistant.components.emby pyemby==1.7 From 2356c1e52ac27cbe8d1b761d3e8c0d1bded49c3b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Jul 2021 11:52:53 +0200 Subject: [PATCH 733/750] Update frontend to 20210706.0 (#52577) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 42ade2d4ae9..c7283a9503a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210630.0" + "home-assistant-frontend==20210706.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98ce2b67183..c35ff252b52 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index c056c39409a..fea5faf972a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90a700867ca..08a325cc154 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210630.0 +home-assistant-frontend==20210706.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 422de2c56dcbae54c0d39da4bcc04b7fdede2703 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Jul 2021 11:57:50 +0200 Subject: [PATCH 734/750] Bumped version to 2021.7.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a58a3e2789..1726cb6f48a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 631e555e2550462de230a9b96a986253028454c7 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 6 Jul 2021 18:48:48 +0200 Subject: [PATCH 735/750] Update Somfy to reduce calls to /site entrypoint (#51572) Co-authored-by: Franck Nijhof --- homeassistant/components/somfy/__init__.py | 95 +------------------ homeassistant/components/somfy/climate.py | 13 ++- homeassistant/components/somfy/const.py | 1 - homeassistant/components/somfy/coordinator.py | 71 ++++++++++++++ homeassistant/components/somfy/cover.py | 13 ++- homeassistant/components/somfy/entity.py | 73 ++++++++++++++ homeassistant/components/somfy/manifest.json | 2 +- homeassistant/components/somfy/sensor.py | 13 ++- homeassistant/components/somfy/switch.py | 13 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/somfy/test_config_flow.py | 11 +-- 12 files changed, 180 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/somfy/coordinator.py create mode 100644 homeassistant/components/somfy/entity.py diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index cc7499c3492..71d7f7f790c 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -1,5 +1,4 @@ """Support for Somfy hubs.""" -from abc import abstractmethod from datetime import timedelta import logging @@ -8,20 +7,16 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_OPTIMISTIC -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) from . import api, config_flow -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .coordinator import SomfyDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -84,25 +79,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data = hass.data[DOMAIN] - data[API] = api.ConfigEntrySomfyApi(hass, entry, implementation) - - async def _update_all_devices(): - """Update all the devices.""" - devices = await hass.async_add_executor_job(data[API].get_devices) - previous_devices = data[COORDINATOR].data - # Sometimes Somfy returns an empty list. - if not devices and previous_devices: - _LOGGER.debug( - "No devices returned. Assuming the previous ones are still valid" - ) - return previous_devices - return {dev.id: dev for dev in devices} - - coordinator = DataUpdateCoordinator( + coordinator = SomfyDataUpdateCoordinator( hass, _LOGGER, name="somfy device update", - update_method=_update_all_devices, + client=api.ConfigEntrySomfyApi(hass, entry, implementation), update_interval=SCAN_INTERVAL, ) data[COORDINATOR] = coordinator @@ -140,70 +121,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - hass.data[DOMAIN].pop(API, None) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -class SomfyEntity(CoordinatorEntity, Entity): - """Representation of a generic Somfy device.""" - - def __init__(self, coordinator, device_id, somfy_api): - """Initialize the Somfy device.""" - super().__init__(coordinator) - self._id = device_id - self.api = somfy_api - - @property - def device(self): - """Return data for the device id.""" - return self.coordinator.data[self._id] - - @property - def unique_id(self) -> str: - """Return the unique id base on the id returned by Somfy.""" - return self._id - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.name - - @property - def device_info(self): - """Return device specific attributes. - - Implemented by platform classes. - """ - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "model": self.device.type, - "via_device": (DOMAIN, self.device.parent_id), - # For the moment, Somfy only returns their own device. - "manufacturer": "Somfy", - } - - def has_capability(self, capability: str) -> bool: - """Test if device has a capability.""" - capabilities = self.device.capabilities - return bool([c for c in capabilities if c.name == capability]) - - def has_state(self, state: str) -> bool: - """Test if device has a state.""" - states = self.device.states - return bool([c for c in states if c.name == state]) - - @property - def assumed_state(self) -> bool: - """Return if the device has an assumed state.""" - return not bool(self.device.states) - - @callback - def _handle_coordinator_update(self): - """Process an update from the coordinator.""" - self._create_device() - super()._handle_coordinator_update() - - @abstractmethod - def _create_device(self): - """Update the device with the latest data.""" diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index 66602aea3e6..0963321100c 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -23,8 +23,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity SUPPORTED_CATEGORIES = {Category.HVAC.value} @@ -49,10 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy climate platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] climates = [ - SomfyClimate(coordinator, device_id, api) + SomfyClimate(coordinator, device_id) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -63,15 +62,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyClimate(SomfyEntity, ClimateEntity): """Representation of a Somfy thermostat device.""" - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._climate = None self._create_device() def _create_device(self): """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.api) + self._climate = Thermostat(self.device, self.coordinator.client) @property def supported_features(self) -> int: diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index 128d6eb76bb..6c7c23e3ab3 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -2,4 +2,3 @@ DOMAIN = "somfy" COORDINATOR = "coordinator" -API = "api" diff --git a/homeassistant/components/somfy/coordinator.py b/homeassistant/components/somfy/coordinator.py new file mode 100644 index 00000000000..c9633c4fa4d --- /dev/null +++ b/homeassistant/components/somfy/coordinator.py @@ -0,0 +1,71 @@ +"""Helpers to help coordinate updated.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pymfy.api.error import QuotaViolationException, SetupNotFoundException +from pymfy.api.model import Device +from pymfy.api.somfy_api import SomfyApi + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +class SomfyDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Somfy data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + client: SomfyApi, + update_interval: timedelta | None = None, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + ) + self.data = {} + self.client = client + self.site_device = {} + self.last_site_index = -1 + + async def _async_update_data(self) -> dict[str, Device]: + """Fetch Somfy data. + + Somfy only allow one call per minute to /site. There is one exception: 2 calls are allowed after site retrieval. + """ + if not self.site_device: + sites = await self.hass.async_add_executor_job(self.client.get_sites) + if not sites: + return {} + self.site_device = {site.id: [] for site in sites} + + site_id = self._site_id + try: + devices = await self.hass.async_add_executor_job( + self.client.get_devices, site_id + ) + self.site_device[site_id] = devices + except SetupNotFoundException: + del self.site_device[site_id] + return await self._async_update_data() + except QuotaViolationException: + self.logger.warning("Quota violation") + + return {dev.id: dev for devices in self.site_device.values() for dev in devices} + + @property + def _site_id(self): + """Return the next site id to retrieve. + + This tweak is required as Somfy does not allow to call the /site entrypoint more than once per minute. + """ + self.last_site_index = (self.last_site_index + 1) % len(self.site_device) + return list(self.site_device.keys())[self.last_site_index] diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d227bc31227..8ed06b3bcd7 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -21,8 +21,8 @@ from homeassistant.components.cover import ( from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN from homeassistant.helpers.restore_state import RestoreEntity -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value} SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value} @@ -37,10 +37,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy cover platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] covers = [ - SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) + SomfyCover(coordinator, device_id, domain_data[CONF_OPTIMISTIC]) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -51,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Representation of a Somfy cover device.""" - def __init__(self, coordinator, device_id, api, optimistic): + def __init__(self, coordinator, device_id, optimistic): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self.categories = set(self.device.categories) self.optimistic = optimistic self._closed = None @@ -64,7 +63,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): def _create_device(self) -> Blind: """Update the device with the latest data.""" - self._cover = Blind(self.device, self.api) + self._cover = Blind(self.device, self.coordinator.client) @property def supported_features(self) -> int: diff --git a/homeassistant/components/somfy/entity.py b/homeassistant/components/somfy/entity.py new file mode 100644 index 00000000000..88ff86e8849 --- /dev/null +++ b/homeassistant/components/somfy/entity.py @@ -0,0 +1,73 @@ +"""Entity representing a Somfy device.""" + +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN + + +class SomfyEntity(CoordinatorEntity, Entity): + """Representation of a generic Somfy device.""" + + def __init__(self, coordinator, device_id): + """Initialize the Somfy device.""" + super().__init__(coordinator) + self._id = device_id + + @property + def device(self): + """Return data for the device id.""" + return self.coordinator.data[self._id] + + @property + def unique_id(self) -> str: + """Return the unique id base on the id returned by Somfy.""" + return self._id + + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device.name + + @property + def device_info(self): + """Return device specific attributes. + + Implemented by platform classes. + """ + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "model": self.device.type, + "via_device": (DOMAIN, self.device.parent_id), + # For the moment, Somfy only returns their own device. + "manufacturer": "Somfy", + } + + def has_capability(self, capability: str) -> bool: + """Test if device has a capability.""" + capabilities = self.device.capabilities + return bool([c for c in capabilities if c.name == capability]) + + def has_state(self, state: str) -> bool: + """Test if device has a state.""" + states = self.device.states + return bool([c for c in states if c.name == state]) + + @property + def assumed_state(self) -> bool: + """Return if the device has an assumed state.""" + return not bool(self.device.states) + + @callback + def _handle_coordinator_update(self): + """Process an update from the coordinator.""" + self._create_device() + super()._handle_coordinator_update() + + @abstractmethod + def _create_device(self): + """Update the device with the latest data.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 8dad4abd6cc..1adbab49fb2 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.3"], + "requirements": ["pymfy==0.11.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 312c425cf87..1817ba3fd8c 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -6,8 +6,8 @@ from pymfy.api.devices.thermostat import Thermostat from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity SUPPORTED_CATEGORIES = {Category.HVAC.value} @@ -16,10 +16,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy sensor platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] sensors = [ - SomfyThermostatBatterySensor(coordinator, device_id, api) + SomfyThermostatBatterySensor(coordinator, device_id) for device_id, device in coordinator.data.items() if SUPPORTED_CATEGORIES & set(device.categories) ] @@ -33,15 +32,15 @@ class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_unit_of_measurement = PERCENTAGE - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._climate = None self._create_device() def _create_device(self): """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.api) + self._climate = Thermostat(self.device, self.coordinator.client) @property def state(self) -> int: diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index 66eef99d6b5..bd0b1dce5d5 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -4,18 +4,17 @@ from pymfy.api.devices.category import Category from homeassistant.components.switch import SwitchEntity -from . import SomfyEntity -from .const import API, COORDINATOR, DOMAIN +from .const import COORDINATOR, DOMAIN +from .entity import SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy switch platform.""" domain_data = hass.data[DOMAIN] coordinator = domain_data[COORDINATOR] - api = domain_data[API] switches = [ - SomfyCameraShutter(coordinator, device_id, api) + SomfyCameraShutter(coordinator, device_id) for device_id, device in coordinator.data.items() if Category.CAMERA.value in device.categories ] @@ -26,14 +25,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SomfyCameraShutter(SomfyEntity, SwitchEntity): """Representation of a Somfy Camera Shutter device.""" - def __init__(self, coordinator, device_id, api): + def __init__(self, coordinator, device_id): """Initialize the Somfy device.""" - super().__init__(coordinator, device_id, api) + super().__init__(coordinator, device_id) self._create_device() def _create_device(self): """Update the device with the latest data.""" - self.shutter = CameraProtect(self.device, self.api) + self.shutter = CameraProtect(self.device, self.coordinator.client) def turn_on(self, **kwargs) -> None: """Turn the entity on.""" diff --git a/requirements_all.txt b/requirements_all.txt index fea5faf972a..c494ca91096 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1590,7 +1590,7 @@ pymelcloud==2.5.3 pymeteoclimatic==0.0.6 # homeassistant.components.somfy -pymfy==0.9.3 +pymfy==0.11.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08a325cc154..eebd4983754 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -904,7 +904,7 @@ pymelcloud==2.5.3 pymeteoclimatic==0.0.6 # homeassistant.components.somfy -pymfy==0.9.3 +pymfy==0.11.0 # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index b7d78883706..6a1c32e4138 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -82,7 +82,9 @@ async def test_full_flow( }, ) - with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): + with patch( + "homeassistant.components.somfy.async_setup_entry", return_value=True + ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["data"]["auth_implementation"] == DOMAIN @@ -95,12 +97,7 @@ async def test_full_flow( "expires_in": 60, } - assert DOMAIN in hass.config.components - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state is config_entries.ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert len(mock_setup_entry.mock_calls) == 1 async def test_abort_if_authorization_timeout(hass, current_request_with_host): From e1c14b5a30ffab52ab2febf1918d011697432faf Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Jul 2021 10:33:07 -0400 Subject: [PATCH 736/750] Don't raise when setting HVAC mode without a mode ZwaveValue (#52444) * Don't raise an error when setting HVAC mode without a value * change logic based on discord convo and add tests * tweak --- homeassistant/components/zwave_js/climate.py | 16 ++++++------- tests/components/zwave_js/test_climate.py | 24 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 4ef13276fbe..43363538500 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -469,15 +469,15 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if not self._current_mode: - # Thermostat(valve) with no support for setting a mode - raise ValueError( - f"Thermostat {self.entity_id} does not support setting a mode" - ) - hvac_mode_value = self._hvac_modes.get(hvac_mode) - if hvac_mode_value is None: + hvac_mode_id = self._hvac_modes.get(hvac_mode) + if hvac_mode_id is None: raise ValueError(f"Received an invalid hvac mode: {hvac_mode}") - await self.info.node.async_set_value(self._current_mode, hvac_mode_value) + + if not self._current_mode: + # Thermostat(valve) has no support for setting a mode, so we make it a no-op + return + + await self.info.node.async_set_value(self._current_mode, hvac_mode_id) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index f86052b3692..fefa680ce77 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -382,6 +382,30 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat blocking=True, ) + # Test setting illegal mode raises an error + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + # Test that setting HVAC_MODE_HEAT works. If the no-op logic didn't work, this would + # raise an error + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_HEAT, + }, + blocking=True, + ) + assert len(client.async_send_command_no_wait.call_args_list) == 1 args = client.async_send_command_no_wait.call_args_list[0][0][0] assert args["command"] == "node.set_value" From 2c75e3fe99ed4b78ad80579d1d141299c98136c1 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 6 Jul 2021 19:34:56 +0300 Subject: [PATCH 737/750] Fix Sensibo timeout exceptions (#52513) --- homeassistant/components/sensibo/climate.py | 105 ++++++++++---------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 10ceaa39a38..d34ea040cdc 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -40,7 +40,7 @@ from .const import DOMAIN as SENSIBO_DOMAIN _LOGGER = logging.getLogger(__name__) ALL = ["all"] -TIMEOUT = 10 +TIMEOUT = 8 SERVICE_ASSUME_STATE = "assume_state" @@ -91,17 +91,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) devices = [] try: - for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: - devices.append( - SensiboClimate(client, dev, hass.config.units.temperature_unit) - ) + with async_timeout.timeout(TIMEOUT): + for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): + if config[CONF_ID] == ALL or dev["id"] in config[CONF_ID]: + devices.append( + SensiboClimate(client, dev, hass.config.units.temperature_unit) + ) except ( aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, pysensibo.SensiboError, ) as err: - _LOGGER.exception("Failed to connect to Sensibo servers") + _LOGGER.error("Failed to get devices from Sensibo servers") raise PlatformNotReady from err if not devices: @@ -150,6 +151,7 @@ class SensiboClimate(ClimateEntity): self._units = units self._available = False self._do_update(data) + self._failed_update = False @property def supported_features(self): @@ -316,59 +318,35 @@ class SensiboClimate(ClimateEntity): else: return - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "targetTemperature", temperature, self._ac_states - ) + await self._async_set_ac_state_property("targetTemperature", temperature) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "fanLevel", fan_mode, self._ac_states - ) + await self._async_set_ac_state_property("fanLevel", fan_mode) async def async_set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", False, self._ac_states - ) + await self._async_set_ac_state_property("on", False) return # Turn on if not currently on. if not self._ac_states["on"]: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", True, self._ac_states - ) + await self._async_set_ac_state_property("on", True) - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "mode", HA_TO_SENSIBO[hvac_mode], self._ac_states - ) + await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "swing", swing_mode, self._ac_states - ) + await self._async_set_ac_state_property("swing", swing_mode) async def async_turn_on(self): """Turn Sensibo unit on.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", True, self._ac_states - ) + await self._async_set_ac_state_property("on", True) async def async_turn_off(self): """Turn Sensibo unit on.""" - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, "on", False, self._ac_states - ) + await self._async_set_ac_state_property("on", False) async def async_assume_state(self, state): """Set external state.""" @@ -377,14 +355,7 @@ class SensiboClimate(ClimateEntity): ) if change_needed: - with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, - "on", - state != HVAC_MODE_OFF, # value - self._ac_states, - True, # assumed_state - ) + await self._async_set_ac_state_property("on", state != HVAC_MODE_OFF, True) if state in [STATE_ON, HVAC_MODE_OFF]: self._external_state = None @@ -396,7 +367,41 @@ class SensiboClimate(ClimateEntity): try: with async_timeout.timeout(TIMEOUT): data = await self._client.async_get_device(self._id, _FETCH_FIELDS) - self._do_update(data) - except (aiohttp.client_exceptions.ClientError, pysensibo.SensiboError): - _LOGGER.warning("Failed to connect to Sensibo servers") + except ( + aiohttp.client_exceptions.ClientError, + asyncio.TimeoutError, + pysensibo.SensiboError, + ): + if self._failed_update: + _LOGGER.warning( + "Failed to update data for device '%s' from Sensibo servers", + self.name, + ) + self._available = False + self.async_write_ha_state() + return + + _LOGGER.debug("First failed update data for device '%s'", self.name) + self._failed_update = True + return + + self._failed_update = False + self._do_update(data) + + async def _async_set_ac_state_property(self, name, value, assumed_state=False): + """Set AC state.""" + try: + with async_timeout.timeout(TIMEOUT): + await self._client.async_set_ac_state_property( + self._id, name, value, self._ac_states, assumed_state + ) + except ( + aiohttp.client_exceptions.ClientError, + asyncio.TimeoutError, + pysensibo.SensiboError, + ) as err: self._available = False + self.async_write_ha_state() + raise Exception( + f"Failed to set AC state for device {self.name} to Sensibo servers" + ) from err From 90f4b3a4ed5ba09af97ca970bbaeb71fc7b00dfc Mon Sep 17 00:00:00 2001 From: ondras12345 Date: Tue, 6 Jul 2021 16:03:54 +0200 Subject: [PATCH 738/750] Fix update of Xiaomi Miio vacuum taking too long (#52539) Home assistant log would get spammed with messages like Update of vacuum.vacuum_name is taking over 10 seconds every 20 seconds if the vacuum was not reachable through the network. See #52353 --- homeassistant/components/xiaomi_miio/vacuum.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index d0bfc148594..cdd53e784b3 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -507,7 +507,11 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): # Fetch timers separately, see #38285 try: - self._timers = self._device.timer() + # Do not try this if the first fetch timed out. + # Two timeouts take longer than 10 seconds and trigger a warning. + # See #52353 + if self._available: + self._timers = self._device.timer() except DeviceException as exc: _LOGGER.debug( "Unable to fetch timers, this may happen on some devices: %s", exc From 746a52bb27ca42d386aff6ee498134f1387a60ba Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 6 Jul 2021 11:21:25 -0500 Subject: [PATCH 739/750] Fresh attempt at SimpliSafe auto-relogin (#52567) * Fresh attempt at SimpliSafe auto-relogin * Fix tests --- .../components/simplisafe/__init__.py | 62 +++++-------------- .../components/simplisafe/config_flow.py | 17 ++--- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 2 +- .../simplisafe/translations/en.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/simplisafe/test_config_flow.py | 43 ++++++------- 8 files changed, 52 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 983629743e7..01e31633a1a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -2,11 +2,11 @@ import asyncio from uuid import UUID -from simplipy import API +from simplipy import get_api from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -107,14 +107,6 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -@callback -def _async_save_refresh_token(hass, config_entry, token): - """Save a refresh token to the config entry.""" - hass.config_entries.async_update_entry( - config_entry, data={**config_entry.data, CONF_TOKEN: token} - ) - - async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -142,6 +134,9 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = [] + if CONF_PASSWORD not in config_entry.data: + raise ConfigEntryAuthFailed("Config schema change requires re-authentication") + entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -164,19 +159,19 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 websession = aiohttp_client.async_get_clientsession(hass) try: - api = await API.login_via_token( - config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + api = await get_api( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + client_id=client_id, + session=websession, ) - except InvalidCredentialsError: - LOGGER.error("Invalid credentials provided") - return False + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - _async_save_refresh_token(hass, config_entry, api.refresh_token) - - simplisafe = SimpliSafe(hass, api, config_entry) + simplisafe = SimpliSafe(hass, config_entry, api) try: await simplisafe.async_init() @@ -303,10 +298,9 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, api, config_entry): + def __init__(self, hass, config_entry, api): """Initialize.""" self._api = api - self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -383,23 +377,7 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - if self._emergency_refresh_token_used: - raise ConfigEntryAuthFailed( - "Update failed with stored refresh token" - ) - - LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") - self._emergency_refresh_token_used = True - - try: - await self._api.refresh_access_token( - self.config_entry.data[CONF_TOKEN] - ) - return - except SimplipyError as err: - raise UpdateFailed( # pylint: disable=raise-missing-from - f"Error while using stored refresh token: {err}" - ) + raise ConfigEntryAuthFailed("Invalid credentials") from result if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -410,16 +388,6 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") - if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: - _async_save_refresh_token( - self._hass, self.config_entry, self._api.refresh_token - ) - - # If we've reached this point using an emergency refresh token, we're in the - # clear and we can discard it: - if self._emergency_refresh_token_used: - self._emergency_refresh_token_used = False - class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ba51356f770..ac31779175f 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the SimpliSafe component.""" -from simplipy import API +from simplipy import get_api from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -47,7 +47,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): client_id = await async_get_client_id(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass) - return await API.login_via_credentials( + return await get_api( self._username, self._password, client_id=client_id, @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) @@ -89,6 +89,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 79e11828eaa..eff37bf1548 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==10.0.0"], + "requirements": ["simplisafe-python==11.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index ad973261a0e..23f85495025 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index b9e274666bb..331eb65ca83 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/requirements_all.txt b/requirements_all.txt index c494ca91096..31761ada12d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2106,7 +2106,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eebd4983754..b1b9bd84946 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1155,7 +1155,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a048e4b0745..4d438965806 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the SimpliSafe config flow.""" -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, patch from simplipy.errors import ( InvalidCredentialsError, @@ -10,18 +10,11 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry -def mock_api(): - """Mock SimpliSafe API class.""" - api = MagicMock() - type(api).refresh_token = PropertyMock(return_value="12345abc") - return api - - async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -33,7 +26,11 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -49,7 +46,7 @@ async def test_invalid_credentials(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=InvalidCredentialsError), ): result = await hass.config_entries.flow.async_init( @@ -102,7 +99,11 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -118,8 +119,8 @@ async def test_step_reauth(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ), patch("homeassistant.components.simplisafe.config_flow.get_api"), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -141,7 +142,7 @@ async def test_step_user(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -151,7 +152,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -165,7 +166,7 @@ async def test_step_user_mfa(hass): } with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): result = await hass.config_entries.flow.async_init( @@ -174,7 +175,7 @@ async def test_step_user_mfa(hass): assert result["step_id"] == "mfa" with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): # Simulate the user pressing the MFA submit button without having clicked @@ -187,7 +188,7 @@ async def test_step_user_mfa(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -197,7 +198,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -207,7 +208,7 @@ async def test_unknown_error(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=SimplipyError), ): result = await hass.config_entries.flow.async_init( From 40d9541d9b33dd664aaddd83aa33427ab89dd4bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Jul 2021 11:28:23 -0500 Subject: [PATCH 740/750] Revert nmap_tracker to 2021.6 version (#52573) * Revert nmap_tracker to 2021.6 version - Its unlikely we will be able to solve #52565 before release * hassfest --- .coveragerc | 3 +- CODEOWNERS | 1 - .../components/nmap_tracker/__init__.py | 396 +----------------- .../components/nmap_tracker/config_flow.py | 223 ---------- .../components/nmap_tracker/device_tracker.py | 271 +++++------- .../components/nmap_tracker/manifest.json | 12 +- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 10 +- requirements_test_all.txt | 7 - tests/components/nmap_tracker/__init__.py | 1 - .../nmap_tracker/test_config_flow.py | 310 -------------- 11 files changed, 106 insertions(+), 1129 deletions(-) delete mode 100644 homeassistant/components/nmap_tracker/config_flow.py delete mode 100644 tests/components/nmap_tracker/__init__.py delete mode 100644 tests/components/nmap_tracker/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index eb0fbdc1fcf..7c741dc26cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -691,8 +691,7 @@ omit = homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* - homeassistant/components/nmap_tracker/__init__.py - homeassistant/components/nmap_tracker/device_tracker.py + homeassistant/components/nmap_tracker/* homeassistant/components/nmbs/sensor.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 46eb14dd66a..c651e35dcc3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,7 +332,6 @@ homeassistant/components/nextcloud/* @meichthys homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole -homeassistant/components/nmap_tracker/* @bdraco homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff homeassistant/components/noaa_tides/* @jdelaney72 diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 76a7e44f153..da699caaa73 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1,395 +1 @@ -"""The Nmap Tracker integration.""" -from __future__ import annotations - -import asyncio -import contextlib -from dataclasses import dataclass -from datetime import datetime, timedelta -import logging - -import aiohttp -from getmac import get_mac_address -from mac_vendor_lookup import AsyncMacLookup -from nmap import PortScanner, PortScannerError - -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS, EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.dt as dt_util - -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, - DOMAIN, - NMAP_TRACKED_DEVICES, - PLATFORMS, - TRACKER_SCAN_INTERVAL, -) - -# Some version of nmap will fail with 'Assertion failed: htn.toclock_running == true (Target.cc: stopTimeOutClock: 503)\n' -NMAP_TRANSIENT_FAILURE = "Assertion failed: htn.toclock_running == true" -MAX_SCAN_ATTEMPTS = 16 -OFFLINE_SCANS_TO_MARK_UNAVAILABLE = 3 - - -def short_hostname(hostname): - """Return the first part of the hostname.""" - if hostname is None: - return None - return hostname.split(".")[0] - - -def human_readable_name(hostname, vendor, mac_address): - """Generate a human readable name.""" - if hostname: - return short_hostname(hostname) - if vendor: - return f"{vendor} {mac_address[-8:]}" - return f"Nmap Tracker {mac_address}" - - -@dataclass -class NmapDevice: - """Class for keeping track of an nmap tracked device.""" - - mac_address: str - hostname: str - name: str - ipv4: str - manufacturer: str - reason: str - last_update: datetime.datetime - offline_scans: int - - -class NmapTrackedDevices: - """Storage class for all nmap trackers.""" - - def __init__(self) -> None: - """Initialize the data.""" - self.tracked: dict = {} - self.ipv4_last_mac: dict = {} - self.config_entry_owner: dict = {} - - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Nmap Tracker from a config entry.""" - domain_data = hass.data.setdefault(DOMAIN, {}) - devices = domain_data.setdefault(NMAP_TRACKED_DEVICES, NmapTrackedDevices()) - scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) - await scanner.async_setup() - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - _async_untrack_devices(hass, entry) - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok - - -@callback -def _async_untrack_devices(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Remove tracking for devices owned by this config entry.""" - devices = hass.data[DOMAIN][NMAP_TRACKED_DEVICES] - remove_mac_addresses = [ - mac_address - for mac_address, entry_id in devices.config_entry_owner.items() - if entry_id == entry.entry_id - ] - for mac_address in remove_mac_addresses: - if device := devices.tracked.pop(mac_address, None): - devices.ipv4_last_mac.pop(device.ipv4, None) - del devices.config_entry_owner[mac_address] - - -def signal_device_update(mac_address) -> str: - """Signal specific per nmap tracker entry to signal updates in device.""" - return f"{DOMAIN}-device-update-{mac_address}" - - -class NmapDeviceScanner: - """This class scans for devices using nmap.""" - - def __init__(self, hass, entry, devices): - """Initialize the scanner.""" - self.devices = devices - self.home_interval = None - - self._hass = hass - self._entry = entry - - self._scan_lock = None - self._stopping = False - self._scanner = None - - self._entry_id = entry.entry_id - self._hosts = None - self._options = None - self._exclude = None - self._scan_interval = None - self._track_new_devices = None - - self._known_mac_addresses = {} - self._finished_first_scan = False - self._last_results = [] - self._mac_vendor_lookup = None - - async def async_setup(self): - """Set up the tracker.""" - config = self._entry.options - self._track_new_devices = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES) - self._scan_interval = timedelta( - seconds=config.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL) - ) - hosts_list = cv.ensure_list_csv(config[CONF_HOSTS]) - self._hosts = [host for host in hosts_list if host != ""] - excludes_list = cv.ensure_list_csv(config[CONF_EXCLUDE]) - self._exclude = [exclude for exclude in excludes_list if exclude != ""] - self._options = config[CONF_OPTIONS] - self.home_interval = timedelta( - minutes=cv.positive_int(config[CONF_HOME_INTERVAL]) - ) - self._scan_lock = asyncio.Lock() - if self._hass.state == CoreState.running: - await self._async_start_scanner() - return - - self._entry.async_on_unload( - self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STARTED, self._async_start_scanner - ) - ) - registry = er.async_get(self._hass) - self._known_mac_addresses = { - entry.unique_id: entry.original_name - for entry in registry.entities.values() - if entry.config_entry_id == self._entry_id - } - - @property - def signal_device_new(self) -> str: - """Signal specific per nmap tracker entry to signal new device.""" - return f"{DOMAIN}-device-new-{self._entry_id}" - - @property - def signal_device_missing(self) -> str: - """Signal specific per nmap tracker entry to signal a missing device.""" - return f"{DOMAIN}-device-missing-{self._entry_id}" - - @callback - def _async_get_vendor(self, mac_address): - """Lookup the vendor.""" - oui = self._mac_vendor_lookup.sanitise(mac_address)[:6] - return self._mac_vendor_lookup.prefixes.get(oui) - - @callback - def _async_stop(self): - """Stop the scanner.""" - self._stopping = True - - async def _async_start_scanner(self, *_): - """Start the scanner.""" - self._entry.async_on_unload(self._async_stop) - self._entry.async_on_unload( - async_track_time_interval( - self._hass, - self._async_scan_devices, - self._scan_interval, - ) - ) - self._mac_vendor_lookup = AsyncMacLookup() - with contextlib.suppress((asyncio.TimeoutError, aiohttp.ClientError)): - # We don't care of this fails since its only - # improves the data when we don't have it from nmap - await self._mac_vendor_lookup.load_vendors() - self._hass.async_create_task(self._async_scan_devices()) - - def _build_options(self): - """Build the command line and strip out last results that do not need to be updated.""" - options = self._options - if self.home_interval: - boundary = dt_util.now() - self.home_interval - last_results = [ - device for device in self._last_results if device.last_update > boundary - ] - if last_results: - exclude_hosts = self._exclude + [device.ipv4 for device in last_results] - else: - exclude_hosts = self._exclude - else: - last_results = [] - exclude_hosts = self._exclude - if exclude_hosts: - options += f" --exclude {','.join(exclude_hosts)}" - # Report reason - if "--reason" not in options: - options += " --reason" - # Report down hosts - if "-v" not in options: - options += " -v" - self._last_results = last_results - return options - - async def _async_scan_devices(self, *_): - """Scan devices and dispatch.""" - if self._scan_lock.locked(): - _LOGGER.debug( - "Nmap scanning is taking longer than the scheduled interval: %s", - TRACKER_SCAN_INTERVAL, - ) - return - - async with self._scan_lock: - try: - await self._async_run_nmap_scan() - except PortScannerError as ex: - _LOGGER.error("Nmap scanning failed: %s", ex) - - if not self._finished_first_scan: - self._finished_first_scan = True - await self._async_mark_missing_devices_as_not_home() - - async def _async_mark_missing_devices_as_not_home(self): - # After all config entries have finished their first - # scan we mark devices that were not found as not_home - # from unavailable - now = dt_util.now() - for mac_address, original_name in self._known_mac_addresses.items(): - if mac_address in self.devices.tracked: - continue - self.devices.config_entry_owner[mac_address] = self._entry_id - self.devices.tracked[mac_address] = NmapDevice( - mac_address, - None, - original_name, - None, - self._async_get_vendor(mac_address), - "Device not found in initial scan", - now, - 1, - ) - async_dispatcher_send(self._hass, self.signal_device_missing, mac_address) - - def _run_nmap_scan(self): - """Run nmap and return the result.""" - options = self._build_options() - if not self._scanner: - self._scanner = PortScanner() - _LOGGER.debug("Scanning %s with args: %s", self._hosts, options) - for attempt in range(MAX_SCAN_ATTEMPTS): - try: - result = self._scanner.scan( - hosts=" ".join(self._hosts), - arguments=options, - timeout=TRACKER_SCAN_INTERVAL * 10, - ) - break - except PortScannerError as ex: - if attempt < (MAX_SCAN_ATTEMPTS - 1) and NMAP_TRANSIENT_FAILURE in str( - ex - ): - _LOGGER.debug("Nmap saw transient error %s", NMAP_TRANSIENT_FAILURE) - continue - raise - _LOGGER.debug( - "Finished scanning %s with args: %s", - self._hosts, - options, - ) - return result - - @callback - def _async_increment_device_offline(self, ipv4, reason): - """Mark an IP offline.""" - if not (formatted_mac := self.devices.ipv4_last_mac.get(ipv4)): - return - if not (device := self.devices.tracked.get(formatted_mac)): - # Device was unloaded - return - device.offline_scans += 1 - if device.offline_scans < OFFLINE_SCANS_TO_MARK_UNAVAILABLE: - return - device.reason = reason - async_dispatcher_send(self._hass, signal_device_update(formatted_mac), False) - del self.devices.ipv4_last_mac[ipv4] - - async def _async_run_nmap_scan(self): - """Scan the network for devices and dispatch events.""" - result = await self._hass.async_add_executor_job(self._run_nmap_scan) - if self._stopping: - return - - devices = self.devices - entry_id = self._entry_id - now = dt_util.now() - for ipv4, info in result["scan"].items(): - status = info["status"] - reason = status["reason"] - if status["state"] != "up": - self._async_increment_device_offline(ipv4, reason) - continue - # Mac address only returned if nmap ran as root - mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) - if mac is None: - self._async_increment_device_offline(ipv4, "No MAC address found") - _LOGGER.info("No MAC address found for %s", ipv4) - continue - - formatted_mac = format_mac(mac) - new = formatted_mac not in devices.tracked - if ( - new - and not self._track_new_devices - and formatted_mac not in devices.tracked - and formatted_mac not in self._known_mac_addresses - ): - continue - - if ( - devices.config_entry_owner.setdefault(formatted_mac, entry_id) - != entry_id - ): - continue - - hostname = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 - vendor = info.get("vendor", {}).get(mac) or self._async_get_vendor(mac) - name = human_readable_name(hostname, vendor, mac) - device = NmapDevice( - formatted_mac, hostname, name, ipv4, vendor, reason, now, 0 - ) - - devices.tracked[formatted_mac] = device - devices.ipv4_last_mac[ipv4] = formatted_mac - self._last_results.append(device) - - if new: - async_dispatcher_send(self._hass, self.signal_device_new, formatted_mac) - else: - async_dispatcher_send( - self._hass, signal_device_update(formatted_mac), True - ) +"""The nmap_tracker component.""" diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py deleted file mode 100644 index 68e61745b63..00000000000 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Config flow for Nmap Tracker integration.""" -from __future__ import annotations - -from ipaddress import ip_address, ip_network, summarize_address_range -from typing import Any - -import ifaddr -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv -from homeassistant.util import get_local_ip - -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, - DOMAIN, - TRACKER_SCAN_INTERVAL, -) - -DEFAULT_NETWORK_PREFIX = 24 - - -def get_network(): - """Search adapters for the network.""" - adapters = ifaddr.get_adapters() - local_ip = get_local_ip() - network_prefix = ( - get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX - ) - return str(ip_network(f"{local_ip}/{network_prefix}", False)) - - -def get_ip_prefix_from_adapters(local_ip, adapters): - """Find the network prefix for an adapter.""" - for adapter in adapters: - for ip_cfg in adapter.ips: - if local_ip == ip_cfg.ip: - return ip_cfg.network_prefix - - -def _normalize_ips_and_network(hosts_str): - """Check if a list of hosts are all ips or ip networks.""" - - normalized_hosts = [] - hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""] - - for host in sorted(hosts): - try: - start, end = host.split("-", 1) - if "." not in end: - ip_1, ip_2, ip_3, _ = start.split(".", 3) - end = ".".join([ip_1, ip_2, ip_3, end]) - summarize_address_range(ip_address(start), ip_address(end)) - except ValueError: - pass - else: - normalized_hosts.append(host) - continue - - try: - ip_addr = ip_address(host) - except ValueError: - pass - else: - normalized_hosts.append(str(ip_addr)) - continue - - try: - network = ip_network(host) - except ValueError: - return None - else: - normalized_hosts.append(str(network)) - - return normalized_hosts - - -def normalize_input(user_input): - """Validate hosts and exclude are valid.""" - errors = {} - normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) - if not normalized_hosts: - errors[CONF_HOSTS] = "invalid_hosts" - else: - user_input[CONF_HOSTS] = ",".join(normalized_hosts) - - normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE]) - if normalized_exclude is None: - errors[CONF_EXCLUDE] = "invalid_hosts" - else: - user_input[CONF_EXCLUDE] = ",".join(normalized_exclude) - - return errors - - -async def _async_build_schema_with_user_input(hass, user_input, include_options): - hosts = user_input.get(CONF_HOSTS, await hass.async_add_executor_job(get_network)) - exclude = user_input.get( - CONF_EXCLUDE, await hass.async_add_executor_job(get_local_ip) - ) - schema = { - vol.Required(CONF_HOSTS, default=hosts): str, - vol.Required( - CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0) - ): int, - vol.Optional(CONF_EXCLUDE, default=exclude): str, - vol.Optional( - CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS) - ): str, - } - if include_options: - schema.update( - { - vol.Optional( - CONF_TRACK_NEW, - default=user_input.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES), - ): bool, - vol.Optional( - CONF_SCAN_INTERVAL, - default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL), - ): vol.All(vol.Coerce(int), vol.Range(min=10, max=3600)), - } - ) - return vol.Schema(schema) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for homekit.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.options = dict(config_entry.options) - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - errors = {} - if user_input is not None: - errors = normalize_input(user_input) - self.options.update(user_input) - - if not errors: - return self.async_create_entry( - title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.options - ) - - return self.async_show_form( - step_id="init", - data_schema=await _async_build_schema_with_user_input( - self.hass, self.options, True - ), - errors=errors, - ) - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Nmap Tracker.""" - - VERSION = 1 - - def __init__(self): - """Initialize config flow.""" - self.options = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - errors = {} - if user_input is not None: - if not self._async_is_unique_host_list(user_input): - return self.async_abort(reason="already_configured") - - errors = normalize_input(user_input) - self.options.update(user_input) - - if not errors: - return self.async_create_entry( - title=f"Nmap Tracker {user_input[CONF_HOSTS]}", - data={}, - options=user_input, - ) - - return self.async_show_form( - step_id="user", - data_schema=await _async_build_schema_with_user_input( - self.hass, self.options, False - ), - errors=errors, - ) - - def _async_is_unique_host_list(self, user_input): - hosts = _normalize_ips_and_network(user_input[CONF_HOSTS]) - for entry in self._async_current_entries(): - if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts: - return False - return True - - async def async_step_import(self, user_input=None): - """Handle import from yaml.""" - if not self._async_is_unique_host_list(user_input): - return self.async_abort(reason="already_configured") - - normalize_input(user_input) - - return self.async_create_entry( - title=f"Nmap Tracker {user_input[CONF_HOSTS]}", data={}, options=user_input - ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 350e75adf48..69c65873e51 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,40 +1,29 @@ """Support for scanning a network with nmap.""" - +from collections import namedtuple +from datetime import timedelta import logging -from typing import Callable +from getmac import get_mac_address +from nmap import PortScanner, PortScannerError import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, - PLATFORM_SCHEMA, - SOURCE_TYPE_ROUTER, -) -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( - CONF_NEW_DEVICE_DEFAULTS, - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import NmapDeviceScanner, short_hostname, signal_device_update -from .const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DEFAULT_TRACK_NEW_DEVICES, DOMAIN, - TRACKER_SCAN_INTERVAL, + PLATFORM_SCHEMA, + DeviceScanner, ) +from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +# Interval in minutes to exclude devices from a scan while they are home +CONF_HOME_INTERVAL = "home_interval" +CONF_OPTIONS = "scan_options" +DEFAULT_OPTIONS = "-F --host-timeout 5s" + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOSTS): cv.ensure_list, @@ -45,164 +34,100 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_scanner(hass, config): +def get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - validated_config = config[DEVICE_TRACKER_DOMAIN] + return NmapDeviceScanner(config[DOMAIN]) - if CONF_SCAN_INTERVAL in validated_config: - scan_interval = validated_config[CONF_SCAN_INTERVAL].total_seconds() - else: - scan_interval = TRACKER_SCAN_INTERVAL - import_config = { - CONF_HOSTS: ",".join(validated_config[CONF_HOSTS]), - CONF_HOME_INTERVAL: validated_config[CONF_HOME_INTERVAL], - CONF_EXCLUDE: ",".join(validated_config[CONF_EXCLUDE]), - CONF_OPTIONS: validated_config[CONF_OPTIONS], - CONF_SCAN_INTERVAL: scan_interval, - CONF_TRACK_NEW: validated_config.get(CONF_NEW_DEVICE_DEFAULTS, {}).get( - CONF_TRACK_NEW, DEFAULT_TRACK_NEW_DEVICES - ), - } +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=import_config, + +class NmapDeviceScanner(DeviceScanner): + """This class scans for devices using nmap.""" + + exclude = [] + + def __init__(self, config): + """Initialize the scanner.""" + self.last_results = [] + + self.hosts = config[CONF_HOSTS] + self.exclude = config[CONF_EXCLUDE] + minutes = config[CONF_HOME_INTERVAL] + self._options = config[CONF_OPTIONS] + self.home_interval = timedelta(minutes=minutes) + + _LOGGER.debug("Scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + _LOGGER.debug("Nmap last results %s", self.last_results) + + return [device.mac for device in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + filter_named = [ + result.name for result in self.last_results if result.mac == device + ] + + if filter_named: + return filter_named[0] + return None + + def get_extra_attributes(self, device): + """Return the IP of the given device.""" + filter_ip = next( + (result.ip for result in self.last_results if result.mac == device), None ) - ) + return {"ip": filter_ip} - _LOGGER.warning( - "Your Nmap Tracker configuration has been imported into the UI, " - "please remove it from configuration.yaml. " - ) + def _update_info(self): + """Scan the network for devices. + Returns boolean if scanning successful. + """ + _LOGGER.debug("Scanning") -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable -) -> None: - """Set up device tracker for Nmap Tracker component.""" - nmap_tracker = hass.data[DOMAIN][entry.entry_id] + scanner = PortScanner() - @callback - def device_new(mac_address): - """Signal a new device.""" - async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, True)]) + options = self._options - @callback - def device_missing(mac_address): - """Signal a missing device.""" - async_add_entities([NmapTrackerEntity(nmap_tracker, mac_address, False)]) + if self.home_interval: + boundary = dt_util.now() - self.home_interval + last_results = [ + device for device in self.last_results if device.last_update > boundary + ] + if last_results: + exclude_hosts = self.exclude + [device.ip for device in last_results] + else: + exclude_hosts = self.exclude + else: + last_results = [] + exclude_hosts = self.exclude + if exclude_hosts: + options += f" --exclude {','.join(exclude_hosts)}" - entry.async_on_unload( - async_dispatcher_connect(hass, nmap_tracker.signal_device_new, device_new) - ) - entry.async_on_unload( - async_dispatcher_connect( - hass, nmap_tracker.signal_device_missing, device_missing - ) - ) + try: + result = scanner.scan(hosts=" ".join(self.hosts), arguments=options) + except PortScannerError: + return False + now = dt_util.now() + for ipv4, info in result["scan"].items(): + if info["status"]["state"] != "up": + continue + name = info["hostnames"][0]["name"] if info["hostnames"] else ipv4 + # Mac address only returned if nmap ran as root + mac = info["addresses"].get("mac") or get_mac_address(ip=ipv4) + if mac is None: + _LOGGER.info("No MAC address found for %s", ipv4) + continue + last_results.append(Device(mac.upper(), name, ipv4, now)) -class NmapTrackerEntity(ScannerEntity): - """An Nmap Tracker entity.""" + self.last_results = last_results - def __init__( - self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool - ) -> None: - """Initialize an nmap tracker entity.""" - self._mac_address = mac_address - self._nmap_tracker = nmap_tracker - self._tracked = self._nmap_tracker.devices.tracked - self._active = active - - @property - def _device(self) -> bool: - """Get latest device state.""" - return self._tracked[self._mac_address] - - @property - def is_connected(self) -> bool: - """Return device status.""" - return self._active - - @property - def name(self) -> str: - """Return device name.""" - return self._device.name - - @property - def unique_id(self) -> str: - """Return device unique id.""" - return self._mac_address - - @property - def ip_address(self) -> str: - """Return the primary ip address of the device.""" - return self._device.ipv4 - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self._mac_address - - @property - def hostname(self) -> str: - """Return hostname of the device.""" - return short_hostname(self._device.hostname) - - @property - def source_type(self) -> str: - """Return tracker source type.""" - return SOURCE_TYPE_ROUTER - - @property - def device_info(self): - """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, - "default_manufacturer": self._device.manufacturer, - "default_name": self.name, - } - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - @property - def icon(self): - """Return device icon.""" - return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" - - @callback - def async_process_update(self, online: bool) -> None: - """Update device.""" - self._active = online - - @property - def extra_state_attributes(self): - """Return the attributes.""" - return { - "last_time_reachable": self._device.last_update.isoformat( - timespec="seconds" - ), - "reason": self._device.reason, - } - - @callback - def async_on_demand_update(self, online: bool): - """Update state.""" - self.async_process_update(online) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Register state update callback.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - signal_device_update(self._mac_address), - self.async_on_demand_update, - ) - ) + _LOGGER.debug("nmap scan successful") + return True diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index ee05843c4fe..9f81c0facaf 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -2,13 +2,7 @@ "domain": "nmap_tracker", "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": [ - "netmap==0.7.0.2", - "getmac==0.8.2", - "ifaddr==0.1.7", - "mac-vendor-lookup==0.1.11" - ], - "codeowners": ["@bdraco"], - "iot_class": "local_polling", - "config_flow": true + "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index aa6d9009574..e71503ce5fc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -176,7 +176,6 @@ FLOWS = [ "netatmo", "nexia", "nightscout", - "nmap_tracker", "notion", "nuheat", "nuki", diff --git a/requirements_all.txt b/requirements_all.txt index 31761ada12d..1065675c13b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,6 @@ ibmiotf==0.3.4 icmplib==3.0 # homeassistant.components.network -# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.iglo @@ -936,9 +935,6 @@ lw12==0.9.2 # homeassistant.components.lyft lyft_rides==0.2 -# homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 - # homeassistant.components.magicseaweed magicseaweed==1.0.3 @@ -1017,9 +1013,6 @@ netdata==0.2.0 # homeassistant.components.discovery netdisco==2.9.0 -# homeassistant.components.nmap_tracker -netmap==0.7.0.2 - # homeassistant.components.nam nettigo-air-monitor==1.0.0 @@ -1869,6 +1862,9 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.nmap_tracker +python-nmap==0.6.1 + # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1b9bd84946..074d78522e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -481,7 +481,6 @@ iaqualink==0.3.90 icmplib==3.0 # homeassistant.components.network -# homeassistant.components.nmap_tracker ifaddr==0.1.7 # homeassistant.components.influxdb @@ -523,9 +522,6 @@ logi_circle==0.2.2 # homeassistant.components.luftdaten luftdaten==0.6.5 -# homeassistant.components.nmap_tracker -mac-vendor-lookup==0.1.11 - # homeassistant.components.maxcube maxcube-api==0.4.3 @@ -574,9 +570,6 @@ nessclient==0.9.15 # homeassistant.components.discovery netdisco==2.9.0 -# homeassistant.components.nmap_tracker -netmap==0.7.0.2 - # homeassistant.components.nam nettigo-air-monitor==1.0.0 diff --git a/tests/components/nmap_tracker/__init__.py b/tests/components/nmap_tracker/__init__.py deleted file mode 100644 index f5e0c85df31..00000000000 --- a/tests/components/nmap_tracker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Nmap Tracker integration.""" diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py deleted file mode 100644 index c4e82936b88..00000000000 --- a/tests/components/nmap_tracker/test_config_flow.py +++ /dev/null @@ -1,310 +0,0 @@ -"""Test the Nmap Tracker config flow.""" -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, - CONF_TRACK_NEW, -) -from homeassistant.components.nmap_tracker.const import ( - CONF_HOME_INTERVAL, - CONF_OPTIONS, - DEFAULT_OPTIONS, - DOMAIN, -) -from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS -from homeassistant.core import CoreState, HomeAssistant - -from tests.common import MockConfigEntry - - -@pytest.mark.parametrize( - "hosts", ["1.1.1.1", "192.168.1.0/24", "192.168.1.0/24,192.168.2.0/24"] -) -async def test_form(hass: HomeAssistant, hosts: str) -> None: - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - schema_defaults = result["data_schema"]({}) - assert CONF_TRACK_NEW not in schema_defaults - assert CONF_SCAN_INTERVAL not in schema_defaults - - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: hosts, - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == f"Nmap Tracker {hosts}" - assert result2["data"] == {} - assert result2["options"] == { - CONF_HOSTS: hosts, - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_range(hass: HomeAssistant) -> None: - """Test we get the form and can take an ip range.""" - 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.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "192.168.0.5-12", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "Nmap Tracker 192.168.0.5-12" - assert result2["data"] == {} - assert result2["options"] == { - CONF_HOSTS: "192.168.0.5-12", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_hosts(hass: HomeAssistant) -> None: - """Test invalid hosts passed in.""" - 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"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "not an ip block", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "form" - assert result2["errors"] == {CONF_HOSTS: "invalid_hosts"} - - -async def test_form_already_configured(hass: HomeAssistant) -> None: - """Test duplicate host list.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" - - -async def test_form_invalid_excludes(hass: HomeAssistant) -> None: - """Test invalid excludes passed in.""" - 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"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_HOSTS: "3.3.3.3", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "not an exclude", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "form" - assert result2["errors"] == {CONF_EXCLUDE: "invalid_hosts"} - - -async def test_options_flow(hass: HomeAssistant) -> None: - """Test we can edit options.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.1.0/24", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - hass.state = CoreState.stopped - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - assert result["data_schema"]({}) == { - CONF_EXCLUDE: "4.4.4.4", - CONF_HOME_INTERVAL: 3, - CONF_HOSTS: "192.168.1.0/24", - CONF_SCAN_INTERVAL: 120, - CONF_OPTIONS: "-F --host-timeout 5s", - CONF_TRACK_NEW: True, - } - - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_HOSTS: "192.168.1.0/24, 192.168.2.0/24", - CONF_HOME_INTERVAL: 5, - CONF_OPTIONS: "-sn", - CONF_EXCLUDE: "4.4.4.4, 5.5.5.5", - CONF_SCAN_INTERVAL: 10, - CONF_TRACK_NEW: False, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == { - CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", - CONF_HOME_INTERVAL: 5, - CONF_OPTIONS: "-sn", - CONF_EXCLUDE: "4.4.4.4,5.5.5.5", - CONF_SCAN_INTERVAL: 10, - CONF_TRACK_NEW: False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import(hass: HomeAssistant) -> None: - """Test we can import from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - with patch( - "homeassistant.components.nmap_tracker.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOSTS: "1.2.3.4/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", - CONF_SCAN_INTERVAL: 2000, - CONF_TRACK_NEW: False, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "Nmap Tracker 1.2.3.4/20" - assert result["data"] == {} - assert result["options"] == { - CONF_HOSTS: "1.2.3.4/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4,6.4.3.2", - CONF_SCAN_INTERVAL: 2000, - CONF_TRACK_NEW: False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_aborts_if_matching(hass: HomeAssistant) -> None: - """Test we can import from yaml.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4", - }, - ) - config_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOSTS: "192.168.0.0/20", - CONF_HOME_INTERVAL: 3, - CONF_OPTIONS: DEFAULT_OPTIONS, - CONF_EXCLUDE: "4.4.4.4, 6.4.3.2", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" From 7a503a6c1f9250f413bc06b4a6100199c5c2ccb6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 6 Jul 2021 17:18:54 +0200 Subject: [PATCH 741/750] Make use of entry id rather than unique id when storing deconz entry in hass.data (#52584) * Make use of entry id rather than unique id when storing entry in hass data * Update homeassistant/components/deconz/services.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/deconz/__init__.py | 7 ++- homeassistant/components/deconz/gateway.py | 4 +- homeassistant/components/deconz/services.py | 49 +++++++++++---------- tests/components/deconz/test_init.py | 10 ++--- tests/components/deconz/test_services.py | 23 +++++++++- 5 files changed, 56 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8b47363c7ba..1b9a418fb29 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -20,8 +20,7 @@ async def async_setup_entry(hass, config_entry): Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data.setdefault(DOMAIN, {}) await async_update_group_unique_id(hass, config_entry) @@ -33,7 +32,7 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False - hass.data[DOMAIN][config_entry.unique_id] = gateway + hass.data[DOMAIN][config_entry.entry_id] = gateway await gateway.async_update_device_registry() @@ -48,7 +47,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - gateway = hass.data[DOMAIN].pop(config_entry.unique_id) + gateway = hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: await async_unload_services(hass) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8b057ab9e51..0a7d7e0c849 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -33,8 +33,8 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): - """Return gateway with a matching bridge id.""" - return hass.data[DECONZ_DOMAIN][config_entry.unique_id] + """Return gateway with a matching config entry ID.""" + return hass.data[DECONZ_DOMAIN][config_entry.entry_id] class DeconzGateway: diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index d524354ff0b..a4f4aec6a76 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -59,14 +59,29 @@ async def async_setup_services(hass): service = service_call.service service_data = service_call.data + gateway = get_master_gateway(hass) + if CONF_BRIDGE_ID in service_data: + found_gateway = False + bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID]) + + for possible_gateway in hass.data[DOMAIN].values(): + if possible_gateway.bridgeid == bridge_id: + gateway = possible_gateway + found_gateway = True + break + + if not found_gateway: + LOGGER.error("Could not find the gateway %s", bridge_id) + return + if service == SERVICE_CONFIGURE_DEVICE: - await async_configure_service(hass, service_data) + await async_configure_service(gateway, service_data) elif service == SERVICE_DEVICE_REFRESH: - await async_refresh_devices_service(hass, service_data) + await async_refresh_devices_service(gateway) elif service == SERVICE_REMOVE_ORPHANED_ENTRIES: - await async_remove_orphaned_entries_service(hass, service_data) + await async_remove_orphaned_entries_service(gateway) hass.services.async_register( DOMAIN, @@ -102,7 +117,7 @@ async def async_unload_services(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES) -async def async_configure_service(hass, data): +async def async_configure_service(gateway, data): """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). @@ -118,10 +133,6 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] @@ -136,31 +147,21 @@ async def async_configure_service(hass, data): await gateway.api.request("put", field, json=data) -async def async_refresh_devices_service(hass, data): +async def async_refresh_devices_service(gateway): """Refresh available devices from deCONZ.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - gateway.ignore_state_updates = True await gateway.api.refresh_state() gateway.ignore_state_updates = False - gateway.async_add_device_callback(NEW_GROUP, force=True) - gateway.async_add_device_callback(NEW_LIGHT, force=True) - gateway.async_add_device_callback(NEW_SCENE, force=True) - gateway.async_add_device_callback(NEW_SENSOR, force=True) + for new_device_type in [NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR]: + gateway.async_add_device_callback(new_device_type, force=True) -async def async_remove_orphaned_entries_service(hass, data): +async def async_remove_orphaned_entries_service(gateway): """Remove orphaned deCONZ entries from device and entity registries.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] - device_registry, entity_registry = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), + gateway.hass.helpers.device_registry.async_get_registry(), + gateway.hass.helpers.entity_registry.async_get_registry(), ) entity_entries = async_entries_for_config_entry( diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 6583372d7bd..814ec588b1e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -61,8 +61,8 @@ async def test_setup_entry_successful(hass, aioclient_mock): config_entry = await setup_deconz_integration(hass, aioclient_mock) assert hass.data[DECONZ_DOMAIN] - assert config_entry.unique_id in hass.data[DECONZ_DOMAIN] - assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master + assert config_entry.entry_id in hass.data[DECONZ_DOMAIN] + assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master async def test_setup_entry_multiple_gateways(hass, aioclient_mock): @@ -80,8 +80,8 @@ async def test_setup_entry_multiple_gateways(hass, aioclient_mock): ) assert len(hass.data[DECONZ_DOMAIN]) == 2 - assert hass.data[DECONZ_DOMAIN][config_entry.unique_id].master - assert not hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master + assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master + assert not hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master async def test_unload_entry(hass, aioclient_mock): @@ -112,7 +112,7 @@ async def test_unload_entry_multiple_gateways(hass, aioclient_mock): assert await async_unload_entry(hass, config_entry) assert len(hass.data[DECONZ_DOMAIN]) == 1 - assert hass.data[DECONZ_DOMAIN][config_entry2.unique_id].master + assert hass.data[DECONZ_DOMAIN][config_entry2.entry_id].master async def test_update_group_unique_id(hass): diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 7ad9c82b08c..8a696da9eb4 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -152,8 +152,27 @@ async def test_configure_service_with_entity_and_field(hass, aioclient_mock): assert aioclient_mock.mock_calls[1][2] == {"on": True, "attr1": 10, "attr2": 20} +async def test_configure_service_with_faulty_bridgeid(hass, aioclient_mock): + """Test that service fails on a bad bridge id.""" + await setup_deconz_integration(hass, aioclient_mock) + aioclient_mock.clear_requests() + + data = { + CONF_BRIDGE_ID: "Bad bridge id", + SERVICE_FIELD: "/lights/1", + SERVICE_DATA: {"on": True}, + } + + await hass.services.async_call( + DECONZ_DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 0 + + async def test_configure_service_with_faulty_field(hass, aioclient_mock): - """Test that service invokes pydeconz with the correct path and data.""" + """Test that service fails on a bad field.""" await setup_deconz_integration(hass, aioclient_mock) data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}} @@ -166,7 +185,7 @@ async def test_configure_service_with_faulty_field(hass, aioclient_mock): async def test_configure_service_with_faulty_entity(hass, aioclient_mock): - """Test that service invokes pydeconz with the correct path and data.""" + """Test that service on a non existing entity.""" await setup_deconz_integration(hass, aioclient_mock) aioclient_mock.clear_requests() From bad2525a6d99168529b7d43a915f283552db92e7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 6 Jul 2021 15:49:22 +0200 Subject: [PATCH 742/750] Fix Fritz Wi-Fi 6 networks with same name as other Wi-Fi (#52588) --- homeassistant/components/fritz/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index d9690b64069..16eaecb178d 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -245,7 +245,7 @@ def wifi_entities_list( ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK) - std_table = {"ac": "5Ghz", "n": "2.4Ghz"} + std_table = {"ax": "Wifi6", "ac": "5Ghz", "n": "2.4Ghz"} networks: dict = {} for i in range(4): if not ("WLANConfiguration" + str(i)) in fritzbox_tools.connection.services: From b14b284e62659f84d3e0c41bbc096b33f992c721 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Jul 2021 18:51:38 +0200 Subject: [PATCH 743/750] Bumped version to 2021.7.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1726cb6f48a..bb46c3b6b87 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From dd26bfb92b52d74c0b2e3aa281d52647353cab89 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Jul 2021 01:34:14 -0700 Subject: [PATCH 744/750] Fix mysensors rgb light (#52604) * remove assert self._white as not all RGB will have a white channel * suggested change * Update homeassistant/components/mysensors/light.py Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof Co-authored-by: Erik Montnemery --- homeassistant/components/mysensors/light.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 81089f052b6..b08d94cebb0 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -132,7 +132,6 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): def _turn_on_rgb_and_w(self, hex_template: str, **kwargs: Any) -> None: """Turn on RGB or RGBW child device.""" assert self._hs - assert self._white is not None rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) @@ -151,8 +150,10 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): if hex_template == "%02x%02x%02x%02x": if new_white is not None: rgb.append(new_white) - else: + elif white is not None: rgb.append(white) + else: + rgb.append(0) hex_color = hex_template % tuple(rgb) if len(rgb) > 3: white = rgb.pop() From a7ee86730c7b57edbb21077222c70821879939ce Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 7 Jul 2021 02:30:48 -0400 Subject: [PATCH 745/750] Bump up ZHA dependencies (#52611) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 68feabb18b4..d37abea2310 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.25.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.58", + "zha-quirks==0.0.59", "zigpy-cc==0.5.2", "zigpy-deconz==0.12.0", "zigpy==0.35.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1065675c13b..c917457162c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2427,7 +2427,7 @@ zengge==0.2 zeroconf==0.32.1 # homeassistant.components.zha -zha-quirks==0.0.58 +zha-quirks==0.0.59 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 074d78522e0..67fba539962 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1327,7 +1327,7 @@ zeep[async]==4.0.0 zeroconf==0.32.1 # homeassistant.components.zha -zha-quirks==0.0.58 +zha-quirks==0.0.59 # homeassistant.components.zha zigpy-cc==0.5.2 From a794c09a0f44dbe375c544b0bf14d85557463ff4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jul 2021 02:23:24 -0500 Subject: [PATCH 746/750] Fix deadlock at shutdown with python 3.9 (#52613) --- homeassistant/runner.py | 10 ---------- homeassistant/util/executor.py | 2 +- tests/util/test_executor.py | 12 ++++++------ 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 86bebecb7b1..5eae0b1b2da 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -73,16 +73,6 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid loop.set_default_executor = warn_use( # type: ignore loop.set_default_executor, "sets default executor on the event loop" ) - - # Shut down executor when we shut down loop - orig_close = loop.close - - def close() -> None: - executor.logged_shutdown() - orig_close() - - loop.close = close # type: ignore - return loop diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index c25c6b9c13f..9277e396bc4 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -62,7 +62,7 @@ def join_or_interrupt_threads( class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): """A ThreadPoolExecutor instance that will not deadlock on shutdown.""" - def logged_shutdown(self) -> None: + def shutdown(self, *args, **kwargs) -> None: # type: ignore """Shutdown backport from cpython 3.9 with interrupt support added.""" with self._shutdown_lock: # type: ignore[attr-defined] self._shutdown = True diff --git a/tests/util/test_executor.py b/tests/util/test_executor.py index 911145ecc4e..eaa48c75d1a 100644 --- a/tests/util/test_executor.py +++ b/tests/util/test_executor.py @@ -24,7 +24,7 @@ async def test_executor_shutdown_can_interrupt_threads(caplog): for _ in range(100): sleep_futures.append(iexecutor.submit(_loop_sleep_in_executor)) - iexecutor.logged_shutdown() + iexecutor.shutdown() for future in sleep_futures: with pytest.raises((concurrent.futures.CancelledError, SystemExit)): @@ -45,13 +45,13 @@ async def test_executor_shutdown_only_logs_max_attempts(caplog): iexecutor.submit(_loop_sleep_in_executor) with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.3): - iexecutor.logged_shutdown() + iexecutor.shutdown() assert "time.sleep(0.2)" in caplog.text assert ( caplog.text.count("is still running at shutdown") == executor.MAX_LOG_ATTEMPTS ) - iexecutor.logged_shutdown() + iexecutor.shutdown() async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog): @@ -65,7 +65,7 @@ async def test_executor_shutdown_does_not_log_shutdown_on_first_attempt(caplog): for _ in range(5): iexecutor.submit(_do_nothing) - iexecutor.logged_shutdown() + iexecutor.shutdown() assert "is still running at shutdown" not in caplog.text @@ -83,9 +83,9 @@ async def test_overall_timeout_reached(caplog): start = time.monotonic() with patch.object(executor, "EXECUTOR_SHUTDOWN_TIMEOUT", 0.5): - iexecutor.logged_shutdown() + iexecutor.shutdown() finish = time.monotonic() assert finish - start < 1 - iexecutor.logged_shutdown() + iexecutor.shutdown() From 998ffeb21d10a643f2e70d8a2a189dbd125662ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Jul 2021 09:46:59 +0200 Subject: [PATCH 747/750] Fix broadlink creating duplicate unique IDs (#52621) --- homeassistant/components/broadlink/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 1f599d6d108..1576c8b8418 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -146,7 +146,6 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._attr_assumed_state = True self._attr_device_class = DEVICE_CLASS_SWITCH self._attr_name = f"{self._device.name} Switch" - self._attr_unique_id = self._device.unique_id @property def is_on(self): @@ -215,6 +214,7 @@ class BroadlinkSP1Switch(BroadlinkSwitch): def __init__(self, device): """Initialize the switch.""" super().__init__(device, 1, 0) + self._attr_unique_id = self._device.unique_id async def _async_send_packet(self, packet): """Send a packet to the device.""" From f7c844d728ebaedcfa973d2852de5fb6028220b9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Jul 2021 10:43:45 +0200 Subject: [PATCH 748/750] Update frontend to 20210707.0 (#52624) --- homeassistant/components/frontend/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/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c7283a9503a..7af6e2bc733 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210706.0" + "home-assistant-frontend==20210707.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c35ff252b52..908cd379886 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.44.0 -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 httpx==0.18.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index c917457162c..5efcc2d3d3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67fba539962..f084564b39e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210706.0 +home-assistant-frontend==20210707.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a048809ca7db0ed7b1304b3216e2a531cffa5d47 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Jul 2021 11:21:23 +0200 Subject: [PATCH 749/750] Bumped version to 2021.7.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bb46c3b6b87..def1d6410e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) From 342366750b0e37338376b71dd68f1e1eada45cd6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Jul 2021 13:09:52 +0200 Subject: [PATCH 750/750] Bumped version to 2021.7.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index def1d6410e5..27eafd0287e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)